logo
Published on

Office UI Fabric Callout in SharePoint Framework (SPFx)

This article explains how to implement the Office UI Fabric Callout in a SharePoint Framework (SPFx) ListView Command Set extension.
The Callout control is useful for displaying inline property panels or edit dialogs directly in the list view without redirecting users to a separate form.


⚙️ Create a new SPFx extension project

Run the following command to create a new ListView Command Set extension:

yo @microsoft/sharepoint

When prompted:

  • Solution Name: FabricCalloutExtension
  • Target Environment: SharePoint Online only (latest)
  • Component Type: Extension
  • Extension Type: ListView Command Set
  • Framework: React

📦 Install dependencies

Install the required Office UI Fabric and SPFx libraries:

npm install office-ui-fabric-react --save
npm install @microsoft/sp-dialog --save
npm install @microsoft/sp-http --save

💻 Implementation

The following code demonstrates how to create a callout dialog using Office UI Fabric's Callout control inside an SPFx extension.

CalloutComponent.tsx

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { ICalloutProps, ICalloutState } from './ICalloutProps';
import { Callout } from 'office-ui-fabric-react/lib/Callout';
import styles01 from './Callout.module.scss';
import { BaseDialog, IDialogConfiguration } from '@microsoft/sp-dialog';
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http'


export default class CalloutComponent extends BaseDialog {
  public itemTitle: string;
  public itemID: number;
  public spcontext?: any | null;

  public render(): void {
    ReactDOM.render(<Cillout itemID={this.itemID} spcontext={this.spcontext} Title={this.itemTitle} domElement={document.activeElement.parentElement} onDismiss={this.onDismiss.bind(this)} />,
      this.domElement);
  }

  public getConfig(): IDialogConfiguration {
    return {
      isBlocking: false
    };
  }

  private onDismiss() {
    ReactDOM.unmountComponentAtNode(this.domElement);
  }
}

class Cillout extends React.Component<ICalloutProps, ICalloutState> {

  constructor(props: ICalloutProps) {
    super(props);
    this.state = {
      Title: this.props.Title
    };

    this.setState({ Title: this.props.Title });
    this._saveClicked = this._saveClicked.bind(this);
    this._onChangedTitle = this._onChangedTitle.bind(this);
  }

  public render(): JSX.Element {
    return (
      <div>
        <Callout
          className={styles01["ms-CalloutExample-callout"]}
          role="alertdialog"
          gapSpace={0}
          target={this.props.domElement}
          onDismiss={this.onDismiss.bind(this)}
          setInitialFocus={true}
          hidden={false}
        >
          <div className={styles01["ms-CalloutExample-header"]}>
            <p className={styles01["ms-CalloutExample-title"]}>
              Property panel
            </p>
          </div>
          <div className={styles01["ms-CalloutExample-inner"]}>
            <div className={styles01["ms-CalloutExample-content"]}>
              <p className={styles01["ms-CalloutExample-subText"]}>
                <TextField label="Title" value={this.state.Title} underlined onChanged={this._onChangedTitle} />
              </p>
            </div>
            <div className={styles01["ms-CalloutExample-actions"]}>
              <PrimaryButton text="Save" onClick={this._saveClicked} />
            </div>
          </div>
        </Callout>
      </div>
    );
  }
  private onDismiss(ev: any) {
    this.props.onDismiss();
  }

  private _onChangedTitle(newValue: string): void {
    this.setState({ Title: newValue });
  }

  private _saveClicked() {
    const body: string = JSON.stringify({
      '__metadata': {
        'type': 'SP.Data.' + this.props.spcontext.pageContext.list.title + 'ListItem'
      },
      'Title': this.state.Title
    });
    this.props.spcontext.spHttpClient.get(this.props.spcontext.pageContext.web.absoluteUrl + `/_api/web/lists/getbytitle('${this.props.spcontext.pageContext.list.title}')/items(` + this.props.itemID + ')', SPHttpClient.configurations.v1).then
      ((Response: SPHttpClientResponse) => {
        this.props.spcontext.spHttpClient.post(this.props.spcontext.pageContext.web.absoluteUrl + `/_api/web/lists/getbytitle('${this.props.spcontext.pageContext.list.title}')/items(` + this.props.itemID + ')', SPHttpClient.configurations.v1,
          {
            headers: {
              'Accept': 'application/json;odata=nometadata',
              'Content-type': 'application/json;odata=verbose',
              'odata-version': '',
              'IF-MATCH': Response.headers.get('ETag'),
              'X-HTTP-Method': 'MERGE'
            },
            body: body
          }).then((response: SPHttpClientResponse) => {
            console.log(`Status code: ${response.status}`);
            console.log(`Status text: ${response.statusText}`);
            this.props.onDismiss();
          });
      });
  }
}

FabricCalloutCommandSet.ts

import { override } from '@microsoft/decorators';
import {
  BaseListViewCommandSet,
  Command,
  IListViewCommandSetListViewUpdatedParameters,
  IListViewCommandSetExecuteEventParameters
} from '@microsoft/sp-listview-extensibility';
import Callout from '../components/Callout'; 

export default class FabricCalloutCommandSet extends BaseListViewCommandSet<{}> {
  @override
  public onInit(): Promise<void> {
    return Promise.resolve();
  }

  @override
  public onListViewUpdated(event: IListViewCommandSetListViewUpdatedParameters): void {
    const compareOneCommand: Command = this.tryGetCommand('COMMAND_1');
    if (compareOneCommand) {
      compareOneCommand.visible = event.selectedRows.length === 1;
    }
  }

  @override
  public onExecute(event: IListViewCommandSetExecuteEventParameters): void {
    switch (event.itemId) {
      case 'COMMAND_1':
        const callout: Callout = new Callout();
        callout.itemTitle=event.selectedRows[0].getValueByName('Title');
        callout.itemID=event.selectedRows[0].getValueByName('ID');
        callout.spcontext= this.context;
        callout.show();
        break;
      default:
        throw new Error('Unknown command');
    }
  }
}

🧠 How it works

  • A custom Callout dialog is displayed when the user selects a list item and clicks the custom command button.
  • The Callout allows inline editing of fields (like Title) without navigating away from the list.
  • The SPHttpClient API is used to update the list item directly.

🚀 Deploy the solution

Once ready, build and deploy the SPFx extension:

gulp build
gulp bundle --ship
gulp package-solution --ship

Upload the generated .sppkg file to your App Catalog and deploy the extension to your site.


Calendar IconBook a demo