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
disableSubmitButtonbooleanRequired. Whether the primary button is dead. With withFooterInput an empty input disables it too, whatever this says.
emptyScreenDescriptionstringRequired. Paragraph under that heading.
emptyScreenHeaderstringRequired. Heading for the empty folder.
emptyScreenImageReact.ReactElement<unknown, string | React.JSXElementConstructor<any>> | stringRequired. Picture for the empty folder. A string is used as an <img> source, an element is rendered as it is.
hasNextPagebooleanRequired. Whether another page can be fetched.
isLoadingbooleanRequired. Initial load: the body is replaced with a skeleton loader
isMultiSelectbooleanRequired. Whether rows carry checkboxes and more than one can be ticked.
isNextPageLoadingbooleanRequired. Whether a page request is in flight, which suppresses another one.
itemsTSelectorItem[]Required. The page of items loaded so far, in the order they are shown.
loadNextPage(startIndex: number) => Promise<void>Required. 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.
onSubmitTOnSubmitRequired. Called with everything the footer holds. Return a promise and the button shows its spinner until it settles. Enter fires it as well.
rowLoaderReact.ReactNodeRequired. Skeleton for a row that has not arrived yet, and for the whole body during the initial load. RowLoader from this folder fits.
searchEmptyScreenDescriptionstringRequired. Paragraph under that heading.
searchEmptyScreenHeaderstringRequired. Heading for a search that found nothing.
searchEmptyScreenImageReact.ReactElement<unknown, string | React.JSXElementConstructor<any>> | stringRequired. The same picture, for a search that found nothing.
submitButtonLabelstringRequired. Text of the primary button. In multi-select the count is appended in brackets, so pass "Add", not "Add (3)".
totalItemsnumberRequired. How many items exist in total, which is what the scrollbar is sized from.
accessRightsTAccessRight[]The choices in that drop-down.
accessRightsModeSelectorAccessRightsModeWhether each entry shows its description.
activeTabIdstringId 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.
alwaysShowFooterbooleanWhether the footer stays put instead of appearing with the first tick.
bodyIsLoadingbooleanIgnored. Selector overwrites it with its own isLoading before the trail ever sees it; the type demands the field all the same.
breadCrumbsTBreadCrumb[]The trail, outermost first. The last entry is the current folder.
breadCrumbsLoaderReact.ReactNodeShown in place of the trail while it loads; BreadCrumbsLoader fits.
cancelButtonIdstringid attribute of that button.
cancelButtonLabelstringText of that button.
classNamestringApplied to the outermost element.
currentFooterInputValuestringInitial text of the field. Selector owns the value from then on and hands the edited one to onSubmit; changing this prop later does nothing.
dataTestIdstringdata-testid of the outermost element. Default: "selector".
descriptionTextstringA line of bold text above the list. It costs the list 32px of height.
disableFirstFetchbooleanWhether to skip the loadNextPage(0) call Selector makes on mount.
displayFileExtensionbooleanWhether a file's extension is drawn after its name, in a dimmer colour.
folderFormValidationRegExpMatched 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.
footerCheckboxLabelstringLabel beside that checkbox.
footerInputHeaderstringLabel above that field.
forceIsMultiSelectbooleanWhether an item's own disableMultiSelect is overruled.
headerPropsHeaderPropsThe header's own props. Required once withHeader is set.
hideBackButtonbooleanWhether the empty screen's "back" link is hidden.
idstringApplied to the outermost element.
infoBarDataTInfoBarDataWhat that bar says. Nothing is drawn without it.
infoTextstringText of that note.
injectedElementReact.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.
isBreadCrumbsLoadingbooleanWhether to replace the trail with breadCrumbsLoader. Default: false.
isCheckedbooleanInitial state of the checkbox. Selector owns it from then on and hands the current value to onSubmit; changing this prop later does nothing.
isContentLoadingbooleanContent refresh: the current body stays on screen dimmed; wins over isLoading
isSearchLoadingbooleanWhether to replace the box with searchLoader.
isSSRbooleanWhether to render every item as plain markup instead of virtualising. The list needs a measured height, which server rendering cannot give it.
maxSelectedItemsnumberLargest 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.
onCloseVoidFunctionCalled 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.
renderCustomItemTRenderCustomItemReplaces the text of a row, keeping its avatar, checkbox and layout.
searchLoaderReact.ReactNodeShown in place of the box while isSearchLoading is set.
searchPlaceholderstringPlaceholder of the box.
searchValuestringText in the box. Searching is yours to do; this is what is displayed.
selectAllIconstringURL of the icon beside it.
selectAllLabelstringText of that row.
selectedAccessRightnull | TAccessRightThe chosen access. Selector copies it into its own state on mount and whenever this changes, and hands that copy to onSubmit.
selectedItemnull | TSelectorItemThe one ticked item outside multi-select, matched by id.
selectedItemsTSelectorItem[]Items to start out ticked, matched by id. Selector copies them into its own state; it is a starting point, not a controlled value.
styleReact.CSSPropertiesApplied to the outermost element.
submitButtonIdstringid attribute of the primary button.
tabsDataTTabItem[]The tabs, in order.
useAsidebooleanWhether 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.
withAccessRightstrueWhether the footer carries an access drop-down. It only accepts true.
withBlurbooleanIgnored. Nothing reads this prop.
withBreadCrumbstrueWhether the folder trail is shown above the list. It only accepts true.
withCancelButtontrueWhether a second, non-primary button sits in the footer. It only accepts true.
withFooterCheckboxtrueWhether the footer carries a checkbox. It only accepts true, and it makes the footer 110px, or 181px alongside withFooterInput.
withFooterInputtrueWhether the footer carries a text field — the "save as" name. It only accepts true, and it makes the footer 145px instead of 73px.
withHeadertrueWhether the panel has a header bar. It only accepts true — leave it out for a selector with no header of its own.
withInfotrueWhether a tinted note sits between the search box and the list. It only accepts true, and the note is hidden during the initial load.
withInfoBadgebooleanWhether an icon is drawn beside the text.
withInfoBarbooleanWhether a dismissable bar is shown above the list.
withoutBackgroundbooleanWhether the backdrop behind the aside is transparent.
withPaddingbooleanWhether the body keeps its 16px of padding at the top. Default: true.
withSearchtrueWhether 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.
withSelectAlltrueWhether 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.
withTabstrueWhether a tab strip is shown above the list. It only accepts true.

headerProps​

Required once withHeader is set.

PropertyTypeDescription
headerLabelstringRequired. Title of the panel.
onCloseClick() => voidRequired. Called by the closing cross.
isCloseablebooleanWhether the closing cross is drawn. Default: true.
onBackClick() => voidCalled by the back arrow.
withoutBackButtonfalseWhether to hide the back arrow. The arrow is drawn only when this is literally false; leaving the prop out hides it, as does true.
withoutBorderbooleanWhether 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
labelstringRequired. Text of the row. The only field every kind of item must carry.
accessnumber | stringThe user's current access, where the row shows one.
avatarReact.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.
colorstringColour of the generated logo, when there is no picture. Colour of the generated logo beside the field.
coverICoverThe room's cover art, which replaces the generated logo. Cover art drawn beside the field.
createDefineRoomTypeRoomTypeWhich room the create link makes, for that empty screen.
defaultInputValuestringText the field starts with.
disabledTextstringText shown at the end of a disabled row, saying why.
disableMultiSelectbooleanWhether this row stays single-select while the rest are not.
displayNamestringFull name, where it differs from label.
dropDownItemsReact.ReactElement<unknown, string | React.JSXElementConstructor<any>>[]Entries of a menu opened instead of calling onCreateClick.
emailstringShown under the name, and handed to renderCustomItem.
fileExststringExtension including the dot; shown after the name with displayFileExtension.
filesCountnumberHow many files it holds.
fileTypeFileTypeWhich kind of document it is, which picks the icon.
foldersCountnumberHow many sub-folders it holds.
forceIsMultiSelectbooleanOverrules this row's own disableMultiSelect.
groupsTUserGroup[]Groups the user belongs to.
hasAvatarbooleanWhether the user has a picture at all, which picks the fallback.
hotkeystringShortcut printed on the right of the row.
iconReact.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).
iconOriginalstringURL of the logo before cropping.
idnumber | stringIdentifies the item. Selection is matched on it throughout, so two rows sharing an id tick and untick together.
isAdminbooleanWhether the user is a full admin.
isCollaboratorbooleanWhether the user is a power user.
isCreateNewItembooleanMarks 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.
isDisabledbooleanWhether the row is greyed out and ignores clicks.
isFolderbooleanMarks the row as a folder, which lets a click open it. Marks the row as openable, as a folder is.
isGroupbooleanMarks the row as a group, which draws the group avatar.
isInputItembooleanMarks 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.
isMCPbooleanMarks the row as an MCP server, which draws the server tile.
isOwnerbooleanWhether the user owns the portal.
isRoomAdminbooleanWhether the user administers a room.
isRoomsOnlybooleanWhether only rooms may be created here, which picks the form-room empty screen.
isSectionSeparatorbooleanMakes that divider a section break, 25px tall.
isSelectedbooleanWhether the row starts out ticked. Selector recomputes it from selectedItems and its own state, so setting it here is a starting point.
isSeparatorbooleanRenders the row as a thin divider, 16px tall, that cannot be clicked.
isSystembooleanWhether 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.
isTemplatebooleanWhether the room is a template, which changes its logo.
isVisitorbooleanWhether the user is a guest.
keystringReact key of the row.
lifetimeTooltipnull | stringTooltip about the file's lifetime.
namestringName of the group.
onAcceptInput(value: string) => voidCalled with the typed name when the tick is clicked or Enter pressed.
onBackClickVoidFunctionCalled by the empty screen's "back" link.
onCancelInputVoidFunctionCalled when the cross is clicked or Escape pressed.
onCreateClickVoidFunctionCalled when the row is clicked, unless dropDownItems is set.
parentIdnumber | stringFolder the file sits in. Folder this one sits in. Folder the room sits in.
placeholderstringPlaceholder of the field.
privatebooleanWhether the room is private, which adds the shield badge.
roleAvatarRoleBadge drawn on the avatar.
roomTypeRoomTypeWhich room it is, which picks the default logo. Which room logo to draw beside the field.
rootFolderTypenumber | stringRoot section it belongs to.
securityFileEntryDtoIntegerAllOfSecurityWhat the current user may do with it.
sharedbooleanWhether the room is shared, which adds the badge on its logo.
specialFolderScopeSpecialFolderScopeMarks the row as a virtual folder such as Recent or Favorites.
statusEmployeeStatusWhether the account is active, pending or disabled.
templateAccessFileShareThe current user's access to that template.
templateIsOwnerbooleanWhether the current user owns that template.
userTypeEmployeeTypeWhich type label is drawn — translated through the app's i18n instance.
viewUrlstringLink the row's title opens.

TBreadCrumb​

PropertyTypeDescription
idnumber | stringRequired. Identifies the crumb; it is what onSelectBreadCrumb is handed back.
labelstringRequired. Text of the crumb.
isAgentbooleanWhether the crumb stands for an agent, which draws the agent's icon.
isRoombooleanWhether the crumb stands for a room, which draws the room's logo.
minWidthstringSmallest width this crumb may shrink to, as a CSS length.
onClickTOnBreadCrumbClickCalled instead of the default navigation when this crumb is clicked.
roomTypeRoomTypeWhich room icon to draw, when isRoom is set.
rootFolderTypeFolderTypeRoot section the crumb belongs to, which picks its icon.
sharedbooleanWhether the room is shared, which adds the badge on its logo.

TAccessRight​

PropertyTypeDescription
accessnumber | stringRequired. The value handed back to you; the component never reads it.
keystringRequired. Identifies the entry within the drop-down.
labelstringRequired. Text of the entry.
descriptionstringSecond line under the label, in the detailed mode.
isSeparatorbooleanWhether this entry is a divider rather than a choice.

TInfoBarData​

PropertyTypeDescription
descriptionReact.ReactNodeRequired. Everything under the title.
titlestringRequired. Bold first line of the bar.
classNamestringApplied to the bar.
iconReact.ReactElement<unknown, string | React.JSXElementConstructor<any>> | stringIcon on the left: a URL for <img>, or an element rendered as it is.
onCloseVoidFunctionCalled 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.