跳到主要内容

TopLoaderService

The thin bar at the top of the page, driven by three static calls rather than by React. It renders nothing: it finds an element you put in the document by id and writes a width onto it every 50ms.

TopLoaderTopLoader

Use this when / not when​

  • Use for a navigation or a fetch whose length you do not know, when the page should stay usable.
  • You must supply the element yourself, with the literal id ipl-progress-indicator, and style it yourself. Nothing in this package renders it and no stylesheet ships with this folder, so without both the calls succeed silently and nothing appears.
  • It is a module-level singleton. One bar per page, one progression at a time; a second caller does not get a second bar.
  • Not for a determinate operation — the bar's numbers are invented from a timer, not from your progress. Use ProgressBar when you know the percentage.
  • Not for the first paint of the application, when there is nothing to keep usable — AppLoader covers the screen instead.

Import​

import { TopLoaderService } from "@onlyoffice/apps-ui-kit/components/top-loading-indicator";

Also exported from the root barrel @onlyoffice/apps-ui-kit.

It needs no provider, and no React at all — the calls work from a router hook, an interceptor or plain script.

Stories​

Default​

Press Start Loading and a thin bar grows along the top of the viewport and stops at 90%; End Loading runs it to the full width and clears it, Cancel clears it at once (start, end, cancel). Use it to see how long a wait looks before the work finishes.

DefaultDefault

Css Customization​

TopLoaderService defines no CSS custom properties -- the element's own style does all of it, as the recipe "The element, and its style" on this page describes. This story renders a static demo bar under a different id, purely for visual reference -- it is not driven by TopLoaderService.

Css CustomizationCss Customization

Minimal example​

import { useEffect, useState } from "react";

import { TopLoaderService } from "@onlyoffice/apps-ui-kit/components/top-loading-indicator";

export function Page() {
const [text, setText] = useState("");

useEffect(() => {
TopLoaderService.start();

const id = setTimeout(() => {
setText("Loaded");
TopLoaderService.end();
}, 1500);

return () => {
clearTimeout(id);
TopLoaderService.cancel();
};
}, []);

return (
<>
<div
id="ipl-progress-indicator"
style={{
position: "fixed",
top: 0,
insetInlineStart: 0,
height: 2,
width: 0,
background: "#4781d1",
transition: "width 0.2s ease-in-out",
zIndex: 9999,
}}
/>
<p>{text}</p>
</>
);
}

Recipes​

Loading​

start() opens the bar and end() closes it. Always pair them, and call cancel() when the work is abandoned — a progression with no end() sits at 90% for ever.

import { useState } from "react";

import { TopLoaderService } from "@onlyoffice/apps-ui-kit/components/top-loading-indicator";
import { Button } from "@onlyoffice/apps-ui-kit/components/button";

export function SaveButton({ save }: { save: () => Promise<void> }) {
const [busy, setBusy] = useState(false);

const onClick = async () => {
setBusy(true);
TopLoaderService.start();
try {
await save();
TopLoaderService.end();
} catch {
TopLoaderService.cancel();
} finally {
setBusy(false);
}
};

return <Button primary label="Save" isLoading={busy} onClick={onClick} />;
}

The element, and its style​

Every visible property is yours. The service writes width in per cent and nothing else, so the element needs a height, a colour and a position of its own, and a transition on width if the steps are not to jump.

export function TopLoaderElement() {
return (
<div
id="ipl-progress-indicator"
style={{
position: "fixed",
top: 0,
insetInlineStart: 0,
width: 0,
height: 4,
background: "#0082c9",
borderRadius: "0 2px 2px 0",
boxShadow: "0 0 8px rgba(0, 130, 201, 0.5)",
transition: "width 0.2s ease-in-out",
zIndex: 9999,
}}
/>
);
}

Behaviour the types don't state​

  • The id is hard-coded. document.getElementById("ipl-progress-indicator") is looked up on every tick; if the element is not there the call does nothing and reports nothing. Render it once, high in the tree, and leave it mounted.
  • The numbers are a timer, not your progress. The bar goes from 0 to 50% over the first second, then adds 10% a second, and stops at 90% until end() is called.
  • end() does not leave a full bar on screen. It animates from where it stands to 100% over one second, and the next tick 50ms later sets the width back to 0px. Nothing fades it out — add a transition or an opacity rule of your own if the finish should be visible.
  • cancel() does not animate. It stops the timer and sets the width to 0px and aria-valuenow to 0 at once, whether the bar is still growing or already closing.
  • end() before start() is ignored, because there is no timer to end.
  • A second start() does not restart the bar. The interval is already running, so the width keeps being computed from the original start time; only cancel() then start() begins again. A start() during the closing animation cancels that animation and snaps the bar backwards.
  • State lives in the module, not in an instance. Two features that both call start() share one progression, and whichever calls end() first finishes it for both.
  • Nothing cleans up after you. The interval runs until end() completes or cancel() is called; unmounting the element leaves the timer ticking against a lookup that now returns null.
  • It is safe to import on the server: every lookup is guarded by typeof document, so calls before hydration are no-ops rather than crashes.
  • The ARIA attributes are written on the first start() — an element that is never started carries none of them.

Accessibility​

  • On the first start() the element is given role="progressbar" with aria-valuemin="0", aria-valuemax="100" and an aria-valuenow updated on every tick. It has no accessible name; add an aria-label to the element yourself.
  • The reported value is invented by the timer, so a screen reader is told a percentage that has nothing to do with the work. For a wait whose length matters, announce the outcome in a live region instead of relying on the bar.
  • Nothing here respects prefers-reduced-motion; the width is animated by your own CSS transition, which is where you can honour it.

Test ids​

ElementAttribute
The bardata-test-id="top-loader"

It is written on the first start(), and note the spelling: data-test-id with two hyphens, not the data-testid every component in this kit uses.