Skip to main content

Selector

Panel for picking one or many things out of a list too long to render at once. It is the portal's people picker, room picker and "save as" browser, and it brings its own search, breadcrumbs, pagination and footer.

SelectorSelector

Use this when / not when​

  • Use to choose users, groups, rooms, folders or files from a list you fetch a page at a time — it virtualises the rows, asks for the next page as they scroll past, and gives you the selection back in one callback.
  • Not for a short, fixed list of options — ComboBox is that, and it costs none of the pagination machinery.
  • Not for a table of data with columns — Table is that.
  • Not as a dialog on its own. Selector has no visibility prop; it fills whatever box you put it in. useAside is the one switch that turns it into a panel, and for anything else wrap it in ModalDialog yourself.

Import​

import { Selector } from "@onlyoffice/apps-ui-kit/components/selector";

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

The folder also exports the three skeletons its own props ask for — RowLoader, SearchLoader, BreadCrumbsLoader — and every type on this page.

Needs ThemeProvider from @onlyoffice/apps-ui-kit/providers/theme, and TranslationProvider from @onlyoffice/apps-ui-kit/providers/translation for the strings Selector prints on its own: the user-type labels beside each person, and the empty screen's "Back" and "Clear filter" links.

Stories​

Default​

A long list that loads 100 rows at a time as you scroll, with one row picked at a time. The first row opens a "New folder" entry and the second is the inline name field for it (isCreateNewItem, isInputItem); change any other prop live in the Controls panel below.

DefaultDefault

Content Loading​

Content refresh state: while new data is loading (search, tab change or folder navigation), the current list stays on screen dimmed and non-interactive instead of being replaced with a skeleton.

Content LoadingContent Loading

Use a folder trail when the list is one level of a folder tree. With more than three folders, the ones between the first and the last two collapse into a menu behind the dots; click an earlier folder and onSelectBreadCrumb reports it, so you can load that folder and pass new items and breadCrumbs.

Bread CrumbsBread Crumbs

New Name​

Use a name field in the footer for a "save as" or copy flow, where the reader picks the destination folder and names the file in one step. The checkbox under the field is a second choice handed to onSubmit with the name (withFooterCheckbox); clear the field and the Add button goes dead.

New NameNew Name

With Header​

Give the panel a header when it stands on its own, in a dialog or a side panel. The title comes with a closing cross and, here, a back arrow for a step-by-step flow (headerProps.withoutBackButton: false); the footer gets a second button that calls onCancel, as Escape does.

With HeaderWith Header

Add a search box when the reader knows the name they are looking for. Type part of a label to narrow the list; type something no label contains, such as zzz, to see the search empty screen (searchEmptyScreenHeader), and clear the box with its cross to get the whole list back. The filtering is the story's own: Selector hands over the query in onSearch and shows what items you give back.

With SearchWith Search

Multi Select​

Use multi-select when the reader adds several items in one go, such as people to a share. Every row gets a checkbox, the footer appears with the first tick and its Add button shows how many are ticked, and the "All items" row above the list ticks or unticks every loaded row (withSelectAll). Two rows start out ticked (selectedItems).

Multi SelectMulti Select

Selection Limit​

Cap the selection when the target can take only so many items. Two rows are ticked and the limit is two, so every other row is greyed out and ignores clicks (maxSelectedItems); untick one and the rest come back. Selector shows no message of its own, so say what the limit is somewhere near the panel.

Selection LimitSelection Limit

Disabled Items​

Keep an item in the list but out of reach when the reader should see it and know why it cannot be picked. The greyed rows ignore clicks and show a reason in place of their checkbox (isDisabled, disabledText on the item).

Disabled ItemsDisabled Items

With Access Rights​

Add an access drop-down to the footer when the items being added need a permission as well. Open it beside the Add button to pick one; the choice is handed to onSubmit with the ticked items. Switch accessRightsMode to detailed in the Controls panel below to open the menu as wide as the footer instead.

With Access RightsWith Access Rights

Empty Folder​

What the reader sees in a folder with nothing in it: the picture, heading and paragraph you pass (emptyScreenImage, emptyScreenHeader, emptyScreenDescription). The "New folder" link is the list's isCreateNewItem row turned into a link, and "Back" goes to the previous folder of the trail; hideBackButton removes it.

Empty FolderEmpty Folder

Loading State​

Show skeletons while the first page is on its way, so the panel keeps its shape instead of flashing an empty screen. The trail, the search box and the list each have a skeleton of their own, and each is switched on separately (isBreadCrumbsLoading, isSearchLoading, isLoading); the folder exports all three loaders.

Loading StateLoading State

With Tabs​

Split the list into tabs when the items come from separate sources. Tick a row, switch to the other tab and tick another: the Add button counts both, because Selector keeps a selection per tab (withTabs, tabsData, activeTabId). Switching tabs is yours to do from each tab's onClick.

With TabsWith Tabs

With Info​

Two ways to say something about the list before the reader picks from it:

  • Only items you can edit are listed here. — a tinted note with an info icon, for a condition that explains what the list holds (withInfo, infoText, withInfoBadge)
  • Recent items — a bold line right above the rows, for a short heading (descriptionText)
With InfoWith Info

With Info Bar​

Put a dismissable bar above the list for a notice the reader can read once and close. The cross appears because the bar has an onClose; hiding the bar when it is clicked is up to you (withInfoBar, infoBarData).

With Info BarWith Info Bar

In Side Panel​

Open Selector as a side panel over the page when picking is a step on its own. The panel slides in from the edge of the window over a dimmed backdrop, and a click on the backdrop calls onClose (useAside); without it, Selector is a plain box that fills its parent.

In Side PanelIn Side Panel

Right To Left​

The panel in a right-to-left layout: the folder trail starts at the right edge with its arrows pointing left, and the row labels and the footer button line up from the right.

Right To LeftRight To Left

Css Customization​

Every overridable variable set on one wrapper -- the variables are listed under CSS variables on this page. Hover a row to see the hover background and hover the tick beside the name field to see its hover colour. The two empty-screen variables are set too but show only when the list is empty.

Css CustomizationCss Customization

Minimal example​

The six empty-screen fields and the pagination four are required even for a list that never paginates, so the smallest working Selector is still a dozen props.

import { useCallback, useState } from "react";
import {
RowLoader,
Selector,
type TSelectorItem,
} from "@onlyoffice/apps-ui-kit/components/selector";

export function PickOnePerson({ people }: { people: TSelectorItem[] }) {
const [chosen, setChosen] = useState<TSelectorItem | null>(null);

const loadNextPage = useCallback(async () => {}, []);

return (
<div style={{ width: 480, height: 485 }}>
<Selector
items={people}
totalItems={people.length}
hasNextPage={false}
isNextPageLoading={false}
isLoading={false}
disableFirstFetch
loadNextPage={loadNextPage}
rowLoader={<RowLoader isContainer />}
isMultiSelect={false}
selectedItem={chosen}
onSelect={(item) => setChosen(item)}
submitButtonLabel="Add"
disableSubmitButton={false}
onSubmit={(selected) => {
console.log(selected);
}}
emptyScreenImage=""
emptyScreenHeader="Nobody here yet"
emptyScreenDescription="Invite somebody and they will show up in this list."
searchEmptyScreenImage=""
searchEmptyScreenHeader="Nothing matches"
searchEmptyScreenDescription="Try a shorter query."
/>
</div>
);
}

Props​

Every with… flag below takes true or nothing at all — withSearch={false} does not type-check. Setting one makes the props listed beside it required, which is how the type says "a search box needs somewhere to send the query".

PropertyTypeDescription
disableSubmitButtonbooleanWhether the primary button is dead. With withFooterInput an empty input disables it too, whatever this says.
emptyScreenDescriptionstringParagraph under that heading.
emptyScreenHeaderstringHeading for the empty folder.
emptyScreenImageReact.ReactElement<unknown, string | React.JSXElementConstructor<any>> | stringPicture for the empty folder. A string is used as an <img> source, an element is rendered as it is.
hasNextPagebooleanWhether another page can be fetched.
isLoadingbooleanInitial load: the body is replaced with a skeleton loader
isMultiSelectbooleanWhether rows carry checkboxes and more than one can be ticked.
isNextPageLoadingbooleanWhether a page request is in flight, which suppresses another one.
itemsTSelectorItem[]The page of items loaded so far, in the order they are shown.
loadNextPage(startIndex: number) => Promise<void>Called with the index to start from when the list nears its end, and once with 0 on mount unless disableFirstFetch is set. Append to items.
onSubmitTOnSubmitCalled with everything the footer holds. Return a promise and the button shows its spinner until it settles. Enter fires it as well.
rowLoaderReact.ReactNodeSkeleton for a row that has not arrived yet, and for the whole body during the initial load. RowLoader from this folder fits.
searchEmptyScreenDescriptionstringParagraph under that heading.
searchEmptyScreenHeaderstringHeading for a search that found nothing.
searchEmptyScreenImageReact.ReactElement<unknown, string | React.JSXElementConstructor<any>> | stringThe same picture, for a search that found nothing.
submitButtonLabelstringText of the primary button. In multi-select the count is appended in brackets, so pass "Add", not "Add (3)".
totalItemsnumberHow many items exist in total, which is what the scrollbar is sized from.
accessRights?TAccessRight[]The choices in that drop-down.
accessRightsMode?SelectorAccessRightsModeWhether each entry shows its description.
activeTabId?stringId of the open tab. Selector keeps one selection per tab id and adds them all up, so switching tabs does not drop what was ticked on the other.
alwaysShowFooter?booleanWhether the footer stays put instead of appearing with the first tick.
bodyIsLoading?booleanIgnored. Selector overwrites it with its own isLoading before the trail ever sees it; the type demands the field all the same.
breadCrumbs?TBreadCrumb[]The trail, outermost first. The last entry is the current folder.
breadCrumbsLoader?React.ReactNodeShown in place of the trail while it loads; BreadCrumbsLoader fits.
cancelButtonId?stringid attribute of that button.
cancelButtonLabel?stringText of that button.
className?stringApplied to the outermost element.
currentFooterInputValue?stringInitial text of the field. Selector owns the value from then on and hands the edited one to onSubmit; changing this prop later does nothing.
dataTestId?stringdata-testid of the outermost element. Default: "selector".
descriptionText?stringA line of bold text above the list. It costs the list 32px of height.
disableFirstFetch?booleanWhether to skip the loadNextPage(0) call Selector makes on mount.
displayFileExtension?booleanWhether a file's extension is drawn after its name, in a dimmer colour.
folderFormValidation?RegExpMatched against the footer input on every keystroke. A match marks the field as in error and shows the kit's "contains special characters" line — so it describes what is forbidden, not what is allowed.
footerCheckboxLabel?stringLabel beside that checkbox.
footerInputHeader?stringLabel above that field.
forceIsMultiSelect?booleanWhether an item's own disableMultiSelect is overruled.
headerProps?HeaderPropsThe header's own props. Required once withHeader is set.
hideBackButton?booleanWhether the empty screen's "back" link is hidden.
id?stringApplied to the outermost element.
infoBarData?TInfoBarDataWhat that bar says. Nothing is drawn without it.
infoText?stringText of that note.
injectedElement?React.ReactElement<unknown, string | React.JSXElementConstructor<any>>Rendered between the breadcrumbs and the search box. Selector clones it to attach a ref and subtracts its measured height from the list.
isBreadCrumbsLoading?booleanWhether to replace the trail with breadCrumbsLoader. Default: false.
isChecked?booleanInitial state of the checkbox. Selector owns it from then on and hands the current value to onSubmit; changing this prop later does nothing.
isContentLoading?booleanContent refresh: the current body stays on screen dimmed; wins over isLoading
isSearchLoading?booleanWhether to replace the box with searchLoader.
isSSR?booleanWhether to render every item as plain markup instead of virtualising. The list needs a measured height, which server rendering cannot give it.
maxSelectedItems?numberLargest number of items that may be ticked at once. Beyond it the unticked rows go grey and stop responding, with no message of their own.
onAccessRightsChange?(access: TAccessRight) => voidCalled when the choice changes.
onCancel?() => voidCalled by that button — and by Escape, which Selector listens for on the window whether or not the button is there.
onClearSearch?(callback?: VoidFunction) => voidCalled by the box's cross and by the empty screen's "clear filter" link. Call the callback to take the selector out of its searching state.
onClose?VoidFunctionCalled by the backdrop and by the aside. Required once useAside is set.
onSearch?(value: string, callback?: VoidFunction) => voidCalled with the trimmed query once typing stops. Call the callback to put the selector into its searching state, which is what swaps the empty screen for the search one. An empty query calls onClearSearch instead.
onSelect?(item: TSelectorItem, isDoubleClick: boolean, doubleClickCallback: () => Promise<void>) => voidCalled when a row is clicked, before Selector updates its own selection. The third argument submits with that one item, which is how a double click confirms a choice.
onSelectAll?() => voidCalled when the row is clicked. Selector ticks and unticks the loaded items itself; this is a notification, not the implementation.
onSelectBreadCrumb?(item: TBreadCrumb) => voidCalled with the crumb that was clicked. Navigating is your job: fetch that folder and hand back new items and breadCrumbs.
renderCustomItem?TRenderCustomItemReplaces the text of a row, keeping its avatar, checkbox and layout.
searchLoader?React.ReactNodeShown in place of the box while isSearchLoading is set.
searchPlaceholder?stringPlaceholder of the box.
searchValue?stringText in the box. Searching is yours to do; this is what is displayed.
selectAllIcon?stringURL of the icon beside it.
selectAllLabel?stringText of that row.
selectedAccessRight?null | TAccessRightThe chosen access. Selector copies it into its own state on mount and whenever this changes, and hands that copy to onSubmit.
selectedItem?null | TSelectorItemThe one ticked item outside multi-select, matched by id.
selectedItems?TSelectorItem[]Items to start out ticked, matched by id. Selector copies them into its own state; it is a starting point, not a controlled value.
style?React.CSSPropertiesApplied to the outermost element.
submitButtonId?stringid attribute of the primary button.
tabsData?TTabItem[]The tabs, in order.
useAside?booleanWhether Selector wraps itself in a backdrop and an Aside. It is the only way the component becomes a panel; on its own it is a plain box that fills its parent.
withAccessRights?trueWhether the footer carries an access drop-down. It only accepts true.
withBlur?booleanIgnored. Nothing reads this prop.
withBreadCrumbs?trueWhether the folder trail is shown above the list. It only accepts true.
withCancelButton?trueWhether a second, non-primary button sits in the footer. It only accepts true.
withFooterCheckbox?trueWhether the footer carries a checkbox. It only accepts true, and it makes the footer 110px, or 181px alongside withFooterInput.
withFooterInput?trueWhether the footer carries a text field — the "save as" name. It only accepts true, and it makes the footer 145px instead of 73px.
withHeader?trueWhether the panel has a header bar. It only accepts true — leave it out for a selector with no header of its own.
withInfo?trueWhether a tinted note sits between the search box and the list. It only accepts true, and the note is hidden during the initial load.
withInfoBadge?booleanWhether an icon is drawn beside the text.
withInfoBar?booleanWhether a dismissable bar is shown above the list.
withoutBackground?booleanWhether the backdrop behind the aside is transparent.
withPadding?booleanWhether the body keeps its 16px of padding at the top. Default: true.
withSearch?trueWhether the list can be searched. It only accepts true. The box hides itself while the list is empty and no search is running, so an empty folder shows the empty screen rather than a search over nothing.
withSelectAll?trueWhether the "select all" row sits above the list. It only accepts true, and the row appears only with isMultiSelect and only while no search is running.
withTabs?trueWhether a tab strip is shown above the list. It only accepts true.

headerProps​

Required once withHeader is set.

PropertyTypeDescription
headerLabelstringTitle of the panel.
onCloseClick() => voidCalled by the closing cross.
isCloseable?booleanWhether the closing cross is drawn. Default: true.
onBackClick?() => voidCalled by the back arrow.
withoutBackButton?falseWhether to hide the back arrow. The arrow is drawn only when this is literally false; leaving the prop out hides it, as does true.
withoutBorder?booleanWhether the line under the header is removed.

TSelectorItem​

One entry of items. Only label is required; which of the rest matter depends on what the row stands for — a person carries email and status, a room carries roomType and shared, and the two marker fields isCreateNewItem and isInputItem turn the row into the create link and the inline name field.

PropertyTypeDescription
labelstringText of the row. The only field every kind of item must carry.
access?number | stringThe user's current access, where the row shows one.
avatar?React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | stringURL of the avatar picture. An empty string draws the default one. Picture drawn instead of the folder icon.
color?stringColour of the generated logo, when there is no picture. Colour of the generated logo beside the field.
cover?ICoverThe room's cover art, which replaces the generated logo. Cover art drawn beside the field.
createDefineRoomType?RoomTypeWhich room the create link makes, for that empty screen.
defaultInputValue?stringText the field starts with.
disabledText?stringText shown at the end of a disabled row, saying why.
disableMultiSelect?booleanWhether this row stays single-select while the rest are not.
displayName?stringFull name, where it differs from label.
dropDownItems?React.ReactElement<unknown, string | React.JSXElementConstructor<any>>[]Entries of a menu opened instead of calling onCreateClick.
email?stringShown under the name, and handed to renderCustomItem.
fileExst?stringExtension including the dot; shown after the name with displayFileExtension.
filesCount?numberHow many files it holds.
fileType?FileTypeWhich kind of document it is, which picks the icon.
foldersCount?numberHow many sub-folders it holds.
forceIsMultiSelect?booleanOverrules this row's own disableMultiSelect.
groups?TUserGroup[]Groups the user belongs to.
hasAvatar?booleanWhether the user has a picture at all, which picks the fallback.
hotkey?stringShortcut printed on the right of the row.
icon?React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | string | TSvgComponentThe icon: a URL, or an SVG component the row renders itself. URL of the room's own logo. Icon beside the field: a URL, or an element rendered as it is. A URL for remotely hosted icons, or a rendered element for the ones bundled as SVG components (see utils/ai/getServerIcon).
iconOriginal?stringURL of the logo before cropping.
id?number | stringIdentifies the item. Selection is matched on it throughout, so two rows sharing an id tick and untick together.
isAdmin?booleanWhether the user is a full admin.
isCollaborator?booleanWhether the user is a power user.
isCreateNewItem?booleanMarks the row as the "create new" entry. It must be first in items, and it is what the empty screen turns into its create link.
isDisabled?booleanWhether the row is greyed out and ignores clicks.
isFolder?booleanMarks the row as a folder, which lets a click open it. Marks the row as openable, as a folder is.
isGroup?booleanMarks the row as a group, which draws the group avatar.
isInputItem?booleanMarks the row as the inline name field. It must be second in items, right after the "create new" row, and it takes over the whole body while it is there.
isMCP?booleanMarks the row as an MCP server, which draws the server tile.
isOwner?booleanWhether the user owns the portal.
isRoomAdmin?booleanWhether the user administers a room.
isRoomsOnly?booleanWhether only rooms may be created here, which picks the form-room empty screen.
isSectionSeparator?booleanMakes that divider a section break, 25px tall.
isSelected?booleanWhether the row starts out ticked. Selector recomputes it from selectedItems and its own state, so setting it here is a starting point.
isSeparator?booleanRenders the row as a thin divider, 16px tall, that cannot be clicked.
isSystem?booleanWhether this is a built-in group such as Everyone. System rows are sorted to the top and a divider is inserted under the first of them.
isTemplate?booleanWhether the room is a template, which changes its logo.
isVisitor?booleanWhether the user is a guest.
key?stringReact key of the row.
lifetimeTooltip?null | stringTooltip about the file's lifetime.
name?stringName of the group.
onAcceptInput?(value: string) => voidCalled with the typed name when the tick is clicked or Enter pressed.
onBackClick?VoidFunctionCalled by the empty screen's "back" link.
onCancelInput?VoidFunctionCalled when the cross is clicked or Escape pressed.
onCreateClick?VoidFunctionCalled when the row is clicked, unless dropDownItems is set.
parentId?number | stringFolder the file sits in. Folder this one sits in. Folder the room sits in.
placeholder?stringPlaceholder of the field.
private?booleanWhether the room is private, which adds the shield badge.
role?AvatarRoleBadge drawn on the avatar.
roomType?RoomTypeWhich room it is, which picks the default logo. Which room logo to draw beside the field.
rootFolderType?number | stringRoot section it belongs to.
security?FileEntryDtoIntegerAllOfSecurityWhat the current user may do with it.
shared?booleanWhether the room is shared, which adds the badge on its logo.
specialFolderScope?SpecialFolderScopeMarks the row as a virtual folder such as Recent or Favorites.
status?EmployeeStatusWhether the account is active, pending or disabled.
templateAccess?FileShareThe current user's access to that template.
templateIsOwner?booleanWhether the current user owns that template.
userType?EmployeeTypeWhich type label is drawn — translated through the app's i18n instance.
viewUrl?stringLink the row's title opens.

TBreadCrumb​

PropertyTypeDescription
idnumber | stringIdentifies the crumb; it is what onSelectBreadCrumb is handed back.
labelstringText of the crumb.
isAgent?booleanWhether the crumb stands for an agent, which draws the agent's icon.
isRoom?booleanWhether the crumb stands for a room, which draws the room's logo.
minWidth?stringSmallest width this crumb may shrink to, as a CSS length.
onClick?TOnBreadCrumbClickCalled instead of the default navigation when this crumb is clicked.
roomType?RoomTypeWhich room icon to draw, when isRoom is set.
rootFolderType?FolderTypeRoot section the crumb belongs to, which picks its icon.
shared?booleanWhether the room is shared, which adds the badge on its logo.

TAccessRight​

PropertyTypeDescription
accessnumber | stringThe value handed back to you; the component never reads it.
keystringIdentifies the entry within the drop-down.
labelstringText of the entry.
description?stringSecond line under the label, in the detailed mode.
isSeparator?booleanWhether this entry is a divider rather than a choice.

TInfoBarData​

PropertyTypeDescription
descriptionReact.ReactNodeEverything under the title.
titlestringBold first line of the bar.
className?stringApplied to the bar.
icon?React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | stringIcon on the left: a URL for <img>, or an element rendered as it is.
onClose?VoidFunctionCalled by the bar's own cross. Without it no cross is drawn.

Enums​

EnumMembers
SelectorAccessRightsModeCompact, Detailed
AvatarRoleowner, admin, guest, user, manager, collaborator, none

Recipes​

Loading​

isLoading replaces the body with rowLoader on the first fetch. isContentLoading is the other one: it leaves the rows where they are and dims them, which is what a search or a folder change wants — and it wins when both are set.

import { useCallback, useState } from "react";
import {
RowLoader,
SearchLoader,
Selector,
type TSelectorItem,
} from "@onlyoffice/apps-ui-kit/components/selector";

export function SearchablePeople({
fetchPage,
}: {
fetchPage: (query: string) => Promise<TSelectorItem[]>;
}) {
const [items, setItems] = useState<TSelectorItem[]>([]);
const [query, setQuery] = useState("");
const [isFirstLoad, setIsFirstLoad] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);

const run = useCallback(
async (next: string, first: boolean) => {
if (first) setIsFirstLoad(true);
else setIsRefreshing(true);

setItems(await fetchPage(next));
setIsFirstLoad(false);
setIsRefreshing(false);
},
[fetchPage],
);

const loadNextPage = useCallback(async () => {
await run(query, true);
}, [query, run]);

return (
<div style={{ width: 480, height: 485 }}>
<Selector
items={items}
totalItems={items.length}
hasNextPage={false}
isNextPageLoading={false}
isLoading={isFirstLoad}
isContentLoading={isRefreshing}
loadNextPage={loadNextPage}
rowLoader={<RowLoader isContainer />}
withSearch
searchLoader={<SearchLoader />}
isSearchLoading={false}
searchValue={query}
searchPlaceholder="Search"
onSearch={(value, entered) => {
setQuery(value);
entered?.();
void run(value, false);
}}
onClearSearch={(left) => {
setQuery("");
left?.();
void run("", false);
}}
isMultiSelect={false}
submitButtonLabel="Add"
disableSubmitButton={false}
onSubmit={(selected) => {
console.log(selected);
}}
emptyScreenImage=""
emptyScreenHeader="Nobody here yet"
emptyScreenDescription="Invite somebody and they will show up in this list."
searchEmptyScreenImage=""
searchEmptyScreenHeader="Nothing matches"
searchEmptyScreenDescription="Try a shorter query."
/>
</div>
);
}

Many at once, with a header and a cancel button​

isMultiSelect puts a checkbox on every row and appends the count to the submit label, so pass "Add", not "Add (3)". The footer appears with the first tick unless alwaysShowFooter is set.

import { useCallback, useState } from "react";
import {
RowLoader,
Selector,
type TSelectorItem,
} from "@onlyoffice/apps-ui-kit/components/selector";

export function PickSeveral({
people,
onDone,
}: {
people: TSelectorItem[];
onDone: (chosen: TSelectorItem[]) => void;
}) {
const [isSaving, setIsSaving] = useState(false);
const loadNextPage = useCallback(async () => {}, []);

return (
<div style={{ width: 480, height: 485 }}>
<Selector
withHeader
headerProps={{
headerLabel: "Add people",
onCloseClick: () => onDone([]),
onBackClick: () => {},
withoutBackButton: false,
withoutBorder: false,
}}
items={people}
totalItems={people.length}
hasNextPage={false}
isNextPageLoading={false}
isLoading={false}
disableFirstFetch
loadNextPage={loadNextPage}
rowLoader={<RowLoader isContainer isMultiSelect />}
isMultiSelect
withSelectAll
selectAllLabel="All accounts"
selectAllIcon=""
onSelectAll={() => {}}
withCancelButton
cancelButtonLabel="Cancel"
onCancel={() => onDone([])}
submitButtonLabel="Add"
disableSubmitButton={isSaving}
onSubmit={async (chosen) => {
setIsSaving(true);
onDone(chosen);
setIsSaving(false);
}}
emptyScreenImage=""
emptyScreenHeader="Nobody here yet"
emptyScreenDescription="Invite somebody and they will show up in this list."
searchEmptyScreenImage=""
searchEmptyScreenHeader="Nothing matches"
searchEmptyScreenDescription="Try a shorter query."
/>
</div>
);
}

As a side panel​

useAside is the only thing that makes Selector a panel: it wraps itself in a Backdrop and an Aside at z-index 310. There is still no visibility prop — mount it when it should be open.

import { useCallback, useState } from "react";
import {
RowLoader,
Selector,
type TSelectorItem,
} from "@onlyoffice/apps-ui-kit/components/selector";

export function PeoplePanel({ people }: { people: TSelectorItem[] }) {
const [isOpen, setIsOpen] = useState(false);
const loadNextPage = useCallback(async () => {}, []);

return (
<>
<button type="button" onClick={() => setIsOpen(true)}>
Members
</button>

{isOpen ? (
<Selector
useAside
onClose={() => setIsOpen(false)}
withHeader
headerProps={{
headerLabel: "Members",
onCloseClick: () => setIsOpen(false),
}}
items={people}
totalItems={people.length}
hasNextPage={false}
isNextPageLoading={false}
isLoading={false}
disableFirstFetch
loadNextPage={loadNextPage}
rowLoader={<RowLoader isContainer />}
isMultiSelect={false}
submitButtonLabel="Add"
disableSubmitButton={false}
onSubmit={() => setIsOpen(false)}
emptyScreenImage=""
emptyScreenHeader="Nobody here yet"
emptyScreenDescription="Invite somebody and they will show up in this list."
searchEmptyScreenImage=""
searchEmptyScreenHeader="Nothing matches"
searchEmptyScreenDescription="Try a shorter query."
/>
) : null}
</>
);
}

"Save as": a folder trail and a file name​

withBreadCrumbs draws the trail above the list and withFooterInput puts a name field in the footer. A crumb click only reports the crumb — loading that folder is yours — and the edited name comes back as onSubmit's third argument.

import { useCallback, useState } from "react";
import {
BreadCrumbsLoader,
RowLoader,
Selector,
type TBreadCrumb,
type TSelectorItem,
} from "@onlyoffice/apps-ui-kit/components/selector";

export function SaveAs({
folders,
trail,
openFolder,
save,
}: {
folders: TSelectorItem[];
trail: TBreadCrumb[];
openFolder: (crumb: TBreadCrumb) => void;
save: (folder: TSelectorItem | undefined, name: string) => void;
}) {
const [folder, setFolder] = useState<TSelectorItem | null>(null);
const loadNextPage = useCallback(async () => {}, []);

return (
<div style={{ width: 480, height: 485 }}>
<Selector
withBreadCrumbs
breadCrumbs={trail}
onSelectBreadCrumb={openFolder}
isBreadCrumbsLoading={false}
breadCrumbsLoader={<BreadCrumbsLoader />}
bodyIsLoading={false}
withFooterInput
footerInputHeader="File name"
currentFooterInputValue="Report.docx"
items={folders}
totalItems={folders.length}
hasNextPage={false}
isNextPageLoading={false}
isLoading={false}
disableFirstFetch
loadNextPage={loadNextPage}
rowLoader={<RowLoader isContainer />}
isMultiSelect={false}
selectedItem={folder}
onSelect={(item) => setFolder(item)}
submitButtonLabel="Save"
disableSubmitButton={false}
onSubmit={(selected, _access, fileName) => save(selected[0], fileName)}
emptyScreenImage=""
emptyScreenHeader="This folder is empty"
emptyScreenDescription="Save here, or pick another folder."
searchEmptyScreenImage=""
searchEmptyScreenHeader="Nothing matches"
searchEmptyScreenDescription="Try a shorter query."
/>
</div>
);
}

Behaviour the types don't state​

  • It needs a parent with a real height. The body measures itself and hands the pixel count to the virtual list; in a box of automatic height the list is zero tall and you see the chrome with nothing under it. isSSR is the escape hatch — it renders every row as plain markup until a height is measured.
  • The list height is arithmetic, not layout. Each part is subtracted from the measured body: search 44px, tabs 33px, breadcrumbs 38px, "select all" 61px, descriptionText 32px, padding 16px, header 54px (70px without tabs), footer 73px — 110px with the checkbox, 145px with the input, 181px with both, plus 20px while the input is in error. A part you style to a different height will not move the list.
  • Selection is seeded, not controlled. selectedItems, selectedItem, isChecked and currentFooterInputValue are copied into Selector's own state; changing them afterwards does not move the ticks, and the current values come back to you through onSubmit, never through a change handler.
  • It fetches on mount. loadNextPage(0) runs in an effect on the first render unless disableFirstFetch is set — including when you have already put a page in items.
  • Escape and Enter are bound to the window, not to the panel: Escape calls onCancel and Enter submits wherever focus happens to be on the page, and both stand down only while the inline name row is open. Two Selectors on one page both answer the same key.
  • The search callback is what shows the search empty screen. onSearch hands you the trimmed query and a callback; until you call it Selector does not consider itself searching, so a query that matches nothing shows the folder empty screen. A query that trims to nothing routes to onClearSearch instead.
  • The back arrow is drawn only for withoutBackButton: false. Leaving the prop out hides it, exactly as true does.
  • Rows are 48px, always. A separator row is 16px, or 25px with isSectionSeparator. Nothing measures a row's content.
  • Two rows have fixed positions. The isCreateNewItem row must be first in items, and the isInputItem row second; the inline name field takes over the whole body while it is there, and the empty screen turns the create row into its link.
  • maxSelectedItems fails silently. Past the limit the unticked rows go grey and stop responding, with no message and no callback.
  • A row with isSystem is sorted to the top and a divider is spliced in beneath it.
  • bodyIsLoading and withBlur are dead. The first is overwritten with Selector's own isLoading before the breadcrumbs see it — the type still demands it once withBreadCrumbs is set. The second is destructured and never read.
  • The strings Selector prints itself — the user-type labels, "Back", "Clear filter" and the footer's "contains special characters" — come from the app's i18n instance, not from props.

CSS variables​

VariableDefaultEffect
--selector-border1px solid greyFooter's top line and the "select all" rule, as a border shorthand
--selector-body-description-texttheme greyColour of descriptionText
--selector-breadcrumbs-prev-item-colortheme greyCrumbs behind the current one, and the dots of a collapsed trail
--selector-breadcrumbs-arrow-right-colortheme greyArrows between crumbs
--selector-info-background-colortheme greyBackground of the withInfo note
--selector-info-colortheme greyText of that note
--selector-item-hover-backgroundtheme greyRow under the pointer
--selector-item-selected-backgroundtheme greyThe ticked row, outside multi-select
--selector-item-disabled-text-colortheme greyLabel of a disabled row
--selector-item-input-button-border1px solid greyTick and cross of the inline name row, and the create-row drop-down, as a border shorthand
--selector-item-input-button-border-hovertheme greyBorder and icon colour of that tick and cross on hover
--selector-empty-screen-description-colortheme greyParagraph on the empty screen
--selector-empty-screen-pressed-button-colortheme greyParagraph on the other empty screen, drawn without withSearch when the create row sets isRoomsOnly with a form-filling or data room type

The two empty-screen variables show only while the list is empty. The theme also defines --selector-empty-screen-button-color and --selector-empty-screen-hover-button-color, but the stylesheet reads neither, so setting them changes nothing.

Accessibility​

  • The list is not reachable from the keyboard. Rows are <div>s with an onClick, carry no role and are never focusable; only the rows near the viewport exist in the DOM at all, so assistive technology is given no list, no count and no position. In multi-select the row's checkbox is focusable with Tab and toggled with Space, and that is the only way through the list; outside multi-select a row can be picked only with the pointer.
  • Focus moves into the selector when it mounts: to the list's scroll container, unless the search box already has it, and to the new-name field when that field appears, with its text selected. Neither scrolls the page (preventScroll), so a selector further down a page takes focus without pulling the page to itself.
  • The search box is given tabIndex={1}, a positive tab index, which pulls it ahead of everything else on the page in tab order.
  • Escape and Enter are listened for on the window, so they act even when focus is outside the panel — and Enter submits without the button being focused.
  • The empty screen's create and back controls are <div>s with click handlers, not buttons.
  • The footer's submit and cancel buttons are native buttons, reached with Tab and pressed with Enter or Space.
  • Give the panel a name yourself: headerProps.headerLabel renders as text, not as a label for the region.

Test ids​

Elementdata-testid
The outermost elementselector, or dataTestId
A rowselector-item-<index>
The submit buttonselector_submit_button
The cancel buttonselector_cancel_button
The inline name rowselector_input_item
Its tick and crossselector_new_item_accept, selector_new_item_cancel
RowLoaderrow-loader, with isContainer
BreadCrumbsLoaderbread-crumbs-loader

Only the first can be overridden by a prop.

  • Aside — the panel useAside wraps this in.
  • SearchInput — the search box, whose onChange gives a string.
  • Table — for rows with columns rather than a single label.