feat: Update card sizing logic for responsive organization chart and bump version to 1.0.6

This commit is contained in:
Torsten Brendgen
2026-08-24 21:36:09 +02:00
parent f3654bc8e0
commit 7f741caa4b
6 changed files with 94 additions and 8 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ SPFx-1.4.1-WebPart für SharePoint Server Subscription Edition. Es erstellt aus
- Profilbild, Anzeigename, Position, Abteilung und optional Kontaktdaten
- Verlinkung auf die SharePoint-Profilseite
- kurzzeitiger In-Memory-Profilcache zur Reduzierung wiederholter REST-Aufrufe
- responsive Baumdarstellung mit SharePoint-Themefarben
- responsive Baumdarstellung mit automatisch kompakteren Karten bei breiten Organisationsebenen
- durchgehende Verbindungslinien und SharePoint-Chevrons zum Auf- und Zuklappen
## Datenquelle und Voraussetzungen
+1 -1
View File
@@ -4,7 +4,7 @@
"name": "organization-chart-webpart",
"title": "organization-chart-webpart",
"id": "b2f88ea8-f7da-4d0e-a945-6973bf1acd1e",
"version": "1.0.5.0",
"version": "1.0.6.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "organization-chart-webpart",
"version": "1.0.5",
"version": "1.0.6",
"private": true,
"main": "lib/index.js",
"engines": { "node": ">=6.9.0 <9.0.0" },
@@ -54,6 +54,26 @@
.status { color: "[theme:bodyText, default:#323130]"; padding: 16px; }
.error { color: "[theme:errorText, default:#a80000]"; padding: 16px; }
.compactTree .tree li, .compactTree li { padding-left: 5px; padding-right: 5px; }
.compactTree .personCard, .compactTree .cardContent { min-height: 82px; }
.compactTree .cardContent { padding: 9px; }
.compactTree .photoWrap { flex-basis: 44px; height: 44px; margin-right: 9px; }
.compactTree .initials { font-size: 15px; line-height: 44px; }
.compactTree .photo { height: 44px; width: 44px; }
.compactTree .name { font-size: 14px; line-height: 18px; }
.compactTree .jobTitle, .compactTree .department, .compactTree .email, .compactTree .location { font-size: 11px; line-height: 15px; }
.denseTree .tree li, .denseTree li { padding-left: 3px; padding-right: 3px; }
.denseTree .personCard, .denseTree .cardContent { min-height: 72px; }
.denseTree .cardContent { padding: 7px; }
.denseTree .photoWrap { flex-basis: 36px; height: 36px; margin-right: 7px; }
.denseTree .initials { font-size: 13px; line-height: 36px; }
.denseTree .photo { height: 36px; width: 36px; }
.denseTree .personText { padding-bottom: 13px; }
.denseTree .name { font-size: 13px; line-height: 17px; }
.denseTree .jobTitle, .denseTree .department, .denseTree .email, .denseTree .location { font-size: 10px; line-height: 14px; }
.denseTree .reportCount { bottom: 6px; font-size: 10px; right: 7px; }
@media (max-width: 600px) {
.tree { display: block; min-width: 0; width: 100%; }
.tree ul { display: block; padding: 12px 0 0 18px; }
@@ -5,23 +5,43 @@ import { IOrganizationChartProps } from './IOrganizationChartProps';
export interface IOrganizationChartState {
collapsed: { [account: string]: boolean };
cardWidth: number;
}
export default class OrganizationChart extends React.Component<IOrganizationChartProps, IOrganizationChartState> {
private viewportElement: HTMLDivElement;
public constructor(props: IOrganizationChartProps) {
super(props);
this.state = { collapsed: {} };
this.state = { collapsed: {}, cardWidth: 230 };
}
public componentDidMount(): void {
this.updateCardWidth();
window.addEventListener('resize', this.onWindowResize);
}
public componentDidUpdate(): void {
this.updateCardWidth();
}
public componentWillUnmount(): void {
window.removeEventListener('resize', this.onWindowResize);
}
public render(): React.ReactElement<IOrganizationChartProps> {
const densityClass: string = this.state.cardWidth <= 170
? styles.denseTree
: (this.state.cardWidth <= 205 ? styles.compactTree : '');
const treeClassName: string = styles.tree + (densityClass ? ' ' + densityClass : '');
return (
<section className={styles.organizationChart} aria-label={this.props.title}>
<div className={styles.header}>
{this.props.title && <h2 className={styles.heading}>{this.props.title}</h2>}
</div>
{this.props.data.truncated && <div className={styles.notice} role='status'>{this.props.truncatedLabel}</div>}
<div className={styles.viewport}>
<div className={styles.tree}>
<div className={styles.viewport} ref={(element: HTMLDivElement): void => { this.viewportElement = element; }}>
<div className={treeClassName}>
<ul role='tree'>{this.renderNode(this.props.data.root, 1)}</ul>
</div>
</div>
@@ -77,8 +97,51 @@ export default class OrganizationChart extends React.Component<IOrganizationChar
</span>
);
return profile.profileUrl
? <a className={styles.personCard} href={profile.profileUrl}>{content}</a>
: <div className={styles.personCard}>{content}</div>;
? <a className={styles.personCard} style={{ width: this.state.cardWidth }} href={profile.profileUrl}>{content}</a>
: <div className={styles.personCard} style={{ width: this.state.cardWidth }}>{content}</div>;
}
private onWindowResize = (): void => {
this.updateCardWidth();
}
private updateCardWidth(): void {
if (!this.viewportElement) { return; }
const availableWidth: number = this.viewportElement.clientWidth - 8;
if (availableWidth <= 0) { return; }
let cardWidth: number;
if (availableWidth <= 600) {
cardWidth = Math.max(144, Math.min(260, availableWidth - 30));
} else {
const maximumBreadth: number = this.getMaximumVisibleBreadth();
const horizontalSpacePerCard: number = 16;
cardWidth = Math.floor((availableWidth - (maximumBreadth * horizontalSpacePerCard)) / maximumBreadth);
cardWidth = Math.max(144, Math.min(230, cardWidth));
}
if (cardWidth !== this.state.cardWidth) {
this.setState({ cardWidth: cardWidth });
}
}
private getMaximumVisibleBreadth(): number {
const countsByLevel: number[] = [];
this.countVisibleNodes(this.props.data.root, 0, countsByLevel);
let maximum: number = 1;
for (let i: number = 0; i < countsByLevel.length; i++) {
maximum = Math.max(maximum, countsByLevel[i]);
}
return maximum;
}
private countVisibleNodes(node: IOrganizationNode, level: number, countsByLevel: number[]): void {
countsByLevel[level] = (countsByLevel[level] || 0) + 1;
const key: string = node.profile.accountName.toLowerCase();
if (this.state.collapsed[key] === true) { return; }
for (let i: number = 0; i < node.children.length; i++) {
this.countVisibleNodes(node.children[i], level + 1, countsByLevel);
}
}
private toggle(key: string): void {
+3
View File
@@ -47,6 +47,9 @@ assert(component.indexOf('peopleCountLabel') < 0 && component.indexOf('&#xE70D;'
component.indexOf('&#xE70E;') >= 0, 'Header count removal or modern toggle icons are missing.');
assert(styles.indexOf('.tree li:only-child { padding-top: 0; }') >= 0,
'Single-child connector spacing must remain continuous.');
assert(component.indexOf('getMaximumVisibleBreadth') >= 0 && component.indexOf('cardWidth') >= 0 &&
styles.indexOf('.compactTree') >= 0 && styles.indexOf('.denseTree') >= 0,
'Responsive organization-chart card sizing is missing.');
assert(component.indexOf("role='presentation'") >= 0, 'Decorative profile images must be hidden from assistive technology.');
['bodyText', 'neutralSecondary', 'neutralLighter', 'themePrimary', 'neutralTertiary', 'white', 'neutralLight', 'focusBorder', 'errorText']
.forEach(function(slot) {