@ui5/webcomponents-react
Version:
React Wrapper for UI5 Web Components and additional components
697 lines (520 loc) • 21.7 kB
Markdown
# @ui5/webcomponents-react
React wrappers for SAP UI5 Web Components plus custom Fiori-compliant React components.
## Installation
```bash
npm install @ui5/webcomponents-react @ui5/webcomponents @ui5/webcomponents-fiori
```
## Setup
Wrap your application with `ThemeProvider`:
```tsx
import { ThemeProvider } from '@ui5/webcomponents-react';
function App() {
return (
<ThemeProvider>
<YourApp />
</ThemeProvider>
);
}
```
**ThemeProvider Props:**
- `children` - Application content
- `staticCssInjected` - Set to `true` when you've imported static CSS (required for SSR/Next.js)
**SSR/Next.js Setup:**
```tsx
// layout.tsx or _app.tsx
import '@ui5/webcomponents-react/styles.css';
<ThemeProvider staticCssInjected>{children}</ThemeProvider>;
```
## TypeScript
### Type Import Patterns
The package provides its own types AND re-uses types from `@ui5/webcomponents`. Follow these patterns:
**Component Props and DOM Refs - import from `@ui5/webcomponents-react`:**
```tsx
// ✅ Props types and DomRef types - from the React package
import type { ButtonPropTypes, ButtonDomRef } from '@ui5/webcomponents-react/Button';
import type { DialogPropTypes, DialogDomRef } from '@ui5/webcomponents-react/Dialog';
import type { SideNavigationPropTypes } from '@ui5/webcomponents-react';
// Usage
const buttonRef = useRef<ButtonDomRef>(null);
// Event handlers - use the prop type's event property (includes detail types)
const handleClick: ButtonPropTypes['onClick'] = (e) => {
// e.detail is fully typed
};
const handleSelectionChange: SideNavigationPropTypes['onSelectionChange'] = (e) => {
console.log(e.detail.item.text); // fully typed
};
```
**Enums and Type Constants - import from `@ui5/webcomponents`:**
```tsx
// ✅ Enums/types - from the web components package directly
import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js';
import ListItemType from '@ui5/webcomponents/dist/types/ListItemType.js';
import ValueState from '@ui5/webcomponents-base/dist/types/ValueState.js';
import MessageStripDesign from '@ui5/webcomponents/dist/types/MessageStripDesign.js';
import NavigationLayoutMode from '@ui5/webcomponents-fiori/dist/types/NavigationLayoutMode.js';
// Usage
<Button design={ButtonDesign.Emphasized}>Click</Button>
<ListItemStandard type={ListItemType.Navigation} />
<MessageStrip design={MessageStripDesign.Information} />
```
### Complete Example
```tsx
import { useState, useRef } from 'react';
import { Button, Dialog, SideNavigation, SideNavigationItem } from '@ui5/webcomponents-react';
import type { ButtonPropTypes, DialogDomRef, SideNavigationPropTypes } from '@ui5/webcomponents-react';
import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js';
import homeIcon from '@ui5/webcomponents-icons/dist/home.js';
function MyComponent() {
const dialogRef = useRef<DialogDomRef>(null);
const [open, setOpen] = useState(false);
const handleClick: ButtonPropTypes['onClick'] = (e) => {
setOpen(true);
};
const handleSelectionChange: SideNavigationPropTypes['onSelectionChange'] = (e) => {
console.log(e.detail.item.text);
};
return (
<>
<Button design={ButtonDesign.Emphasized} onClick={handleClick}>
Open Dialog
</Button>
<Dialog ref={dialogRef} open={open} onClose={() => setOpen(false)}>
Content
</Dialog>
<SideNavigation onSelectionChange={handleSelectionChange}>
<SideNavigationItem text="Home" icon={homeIcon} />
</SideNavigation>
</>
);
}
```
## Side-Effect Imports (Icons, Assets, Features)
UI5 Web Components require certain side-effect imports to function. These register components/assets at runtime.
### ⚠️ CRITICAL: Import Order
**Feature and asset imports MUST come BEFORE any component imports.** They register functionality at module load time - importing them after components means the registration happens too late.
```tsx
// ✅ CORRECT - assets FIRST
import '@ui5/webcomponents-react/dist/Assets.js';
import { Button } from '@ui5/webcomponents-react/Button'; // components after
// ❌ WRONG - components imported before assets
import { Button } from '@ui5/webcomponents-react/Button';
import '@ui5/webcomponents-react/dist/Assets.js'; // TOO LATE!
```
**For SSR/Next.js:** Use a separate file that is imported first:
```tsx
// ui5-config.ts - create this file
import '@ui5/webcomponents-react/dist/Assets.js';
// app entry point (index.tsx, main.tsx, layout.tsx)
import './ui5-config'; // MUST be the first import!
import { ThemeProvider } from '@ui5/webcomponents-react/ThemeProvider';
// ... other imports
```
**SSR/Next.js Critical:** These imports must be in **client components** (`'use client'` directive). In server components, they do nothing.
### Icon Imports
Icons must be imported before use. **Use named imports for better maintainability:**
```tsx
// ✅ Recommended - named imports
import addIcon from '@ui5/webcomponents-icons/dist/add.js';
import deleteIcon from '@ui5/webcomponents-icons/dist/delete.js';
import editIcon from '@ui5/webcomponents-icons/dist/edit.js';
// Use with the imported variable
<Button icon={addIcon}>Add Item</Button>
<Button icon={deleteIcon}>Delete</Button>
// ❌ Avoid - side-effect imports with magic strings
import '@ui5/webcomponents-icons/dist/add.js';
<Button icon="add">Add Item</Button>
```
### Asset Imports (i18n, Theming, CLDR)
**⚠️ ESSENTIAL:** Assets MUST be imported if you need **any** of these features:
- **Translations (i18n)** - Component UI texts in different languages
- **Theme switching** - Changing themes at runtime (Horizon, Fiori 3, dark modes, high contrast)
- **CLDR locale data** - Locale-specific date/time formatting, number formatting, calendar data
Without asset imports, components display English text only, theme switching won't work, and date/time components won't format correctly for different locales.
```tsx
// Standard installation (includes fiori package assets)
import '@ui5/webcomponents-react/dist/Assets.js';
// Minimal installation (without @ui5/webcomponents-fiori)
import '@ui5/webcomponents/dist/Assets.js';
import '@ui5/webcomponents-react/dist/json-imports/i18n.js';
```
Test translations with URL param: `?sap-ui-language=de`
### Feature Imports
Optional features are loaded dynamically on demand. Available features include:
- `InputSuggestions` - Input suggestions while typing
- `ColorPaletteMoreColors` - "More colors" dialog in color palette
- Calendar types: Buddhist, Islamic, Japanese, Persian
See [UI5 Web Components Features](https://ui5.github.io/webcomponents/docs/advanced/using-features/) for details.
## Import Pattern
Use subpath imports for tree-shaking (especially in development):
```tsx
// ✅ Recommended - tree-shakeable
import { Button } from '@ui5/webcomponents-react/Button';
import { Input } from '@ui5/webcomponents-react/Input';
import { Dialog } from '@ui5/webcomponents-react/Dialog';
import { AnalyticalTable } from '@ui5/webcomponents-react/AnalyticalTable';
// ❌ Avoid - imports entire bundle
import { Button, Input, Dialog } from '@ui5/webcomponents-react';
```
**Convert existing imports:**
```bash
npx @ui5/webcomponents-react-cli codemod --transform export-maps --src ./src
```
## Critical: Event Handling
Web components emit **CustomEvents** with data in `e.detail`:
```tsx
// ❌ WRONG - won't work
const handleChange = (e) => {
console.log(e.selectedOption); // undefined!
};
// ✅ CORRECT - use e.detail
const handleChange = (e) => {
console.log(e.detail.selectedOption);
};
// Common event patterns
<Input onInput={(e) => setValue(e.target.value)} />
<Select onChange={(e) => setSelected(e.detail.selectedOption)} />
<List onSelectionChange={(e) => handleSelection(e.detail.selectedItems)} />
<DatePicker onChange={(e) => setDate(e.detail.value)} />
```
## Critical: Slot Props
Custom components in slots **must forward the `slot` prop**:
```tsx
// ❌ WRONG - slot won't render
const CustomHeader = ({ children }) => <div>{children}</div>;
// ✅ CORRECT - forward slot prop
const CustomHeader = ({ children, slot }) => <div slot={slot}>{children}</div>;
// Usage
<Dialog header={<CustomHeader>Title</CustomHeader>} />
<Card header={<CustomHeader>Card Title</CustomHeader>} />
```
**Note:** React Portals are **not supported** in slot props.
## Styling
### CSS Variables (Recommended)
Use SAP CSS variables directly:
```css
.myComponent {
background: var(--sapBackgroundColor);
color: var(--sapTextColor);
border-color: var(--sapField_BorderColor);
border-radius: var(--sapElement_BorderCornerRadius);
font-family: var(--sapFontFamily);
/* Semantic colors */
--success: var(--sapPositiveColor);
--error: var(--sapNegativeColor);
--warning: var(--sapCriticalColor);
--info: var(--sapInformativeColor);
}
```
### Shadow DOM Styling
Web component internals use Shadow DOM. Use `::part()`:
```css
/* ❌ Can't pierce Shadow DOM */
.myButton button {
background: red;
}
/* ✅ Use ::part() */
.myCheckbox::part(root) {
display: flex;
width: unset;
}
.myDialog::part(content) {
padding: 1rem;
}
```
---
## Complex Components - Important Details
### Modals
**Critical:** Only mount ONE `<Modals />` component per application, otherwise multiple dialogs/popovers will display.
```tsx
import { Modals } from '@ui5/webcomponents-react/Modals';
import { ThemeProvider } from '@ui5/webcomponents-react/ThemeProvider';
const root = createRoot(document.getElementById('root'));
root.render(
<ThemeProvider>
<Modals />
<App />
</ThemeProvider>,
);
```
**Available methods:**
- `Modals.showDialog(props, config)` - Returns `{ ref, close }`
- `Modals.showPopover(props, config)` - Returns `{ ref, close }`
- `Modals.showResponsivePopover(props, config)` - Returns `{ ref, close }`
- `Modals.showMenu(props, config)` - Returns `{ ref, close }`
- `Modals.showMessageBox(props, config)` - Returns `{ ref, close }`
- `Modals.showToast(props, config)` - Returns `{ ref }`
---
### AnalyticalTable
High-performance virtualized data table.
#### ⚠️ CRITICAL: Memoization Requirements
The AnalyticalTable is highly sensitive to reference changes. **Non-memoized props cause full re-renders and destroy performance.** The following MUST be memoized:
| Prop/Option | Memoize With | Impact if Not Memoized |
| ----------------------- | ---------------- | ------------------------------------------ |
| `columns` | `useMemo` | **Critical** - full table re-render |
| `data` | `useMemo` | **Critical** - full table re-render |
| `tableHooks` | `useMemo` | Re-initializes all plugin hooks |
| `selectedRowIds` | `useMemo` | Resets selection state on every render |
| `markNavigatedRow` | `useCallback` | Re-renders navigation column |
| `onTableScroll` | `useCallback` | **Critical** - fires on EVERY scroll event |
| `sortType` (in columns) | `useCallback` | Re-sorts on every render |
| `Cell` component | `memo`/`useMemo` | Re-renders all cells |
| `renderRowSubComponent` | `useCallback` | Re-renders all subcomponents |
```tsx
// ❌ WRONG - causes constant re-renders
<AnalyticalTable
columns={[{ accessor: 'name' }]}
data={fetchedData}
tableHooks={[useAnnounceEmptyCells]}
markNavigatedRow={(row) => row.original.id === selectedId}
onTableScroll={(e) => console.log(e)}
/>;
// ✅ CORRECT - properly memoized
const columns = useMemo(
() => [
{
accessor: 'name',
Header: 'Name',
Cell: memo(({ value }) => <span>{value}</span>), // memo for custom Cell
sortType: useCallback((a, b) => a.localeCompare(b), []), // memoize if function
},
],
[],
);
const data = useMemo(() => fetchedData, [fetchedData]);
const tableHooks = useMemo(() => [useAnnounceEmptyCells], []);
const markNavigatedRow = useCallback((row) => row.original.id === selectedId, [selectedId]);
const handleTableScroll = useCallback((e) => {
// Use throttle/debounce for expensive operations!
console.log(e.detail.rows);
}, []);
<AnalyticalTable
columns={columns}
data={data}
tableHooks={tableHooks}
markNavigatedRow={markNavigatedRow}
onTableScroll={handleTableScroll}
/>;
```
**Additional memoization tips:**
- `renderRowSubComponent` - memoize with `useCallback`, and consider memoizing the returned component for tree tables
- Custom `Filter` components in columns should be memoized
- `Popover` component in columns should be memoized
#### Features Without Design Specification / Limitations
The AnalyticalTable has features that do not have a defined UX design specification. To follow UXC guidelines:
| Feature | Status |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| No sticky columns/rows | Not supported due to technical limitations |
| Pop-in behavior | `sap.ui.table` doesn't support pop-in (unlike `sap.m.Table`); unclear if this should be part of design |
| `visibleRowCountMode: "Auto"` | `"AutoWithEmptyRows"` is preferred. `"Auto"` can lead to inconsistent table heights depending on container |
| `alwaysShowBusyIndicator` | Should generally be `true`. Only if loading times are over 1 second is the default skeleton sufficient |
| `scaleWidthMode` | Only default mode is available out of the box for `sap.m.Table` |
| `renderRowSubComponent` | **No design/UX concept** for this functionality |
| `useIndeterminateRowSelection` | **No design/UX concept** for this functionality |
| `useRowDisableSelection` | **Deprecated** - table rows should not be disabled |
#### Limitations
- **Grouping disabled with subcomponents:** When `renderRowSubComponent` is set, `grouping` is disabled.
- **IncludeHeight modes:** `AnalyticalTableSubComponentsBehavior.IncludeHeight` and `IncludeHeightExpandable` do not support `AnalyticalTableVisibleRowCountMode.Interactive`.
- **Custom Cell scaling:** Automatic scaling of custom cells is not supported with `scaleWidthMode: Grow` or `Smart`. Use `scaleWidthModeOptions` column option to pass strings for width calculation.
- **Pop-in columns:** When using `responsivePopIn`, at least one column must remain visible at all times. It's recommended to only offer sorting/filtering/grouping for columns that are always displayed.
- **Hidden columns state reset:** React-table resets hidden column state - set `autoResetHiddenColumns: false` in `reactTableOptions` to prevent this.
#### Preventing Table State Reset on Data Changes
By default, the table resets sorters, filters, grouping, selected rows when data changes:
```tsx
const skipPageResetRef = useRef(false);
const updateData = (newData) => {
skipPageResetRef.current = true;
setData(newData);
};
useEffect(() => {
skipPageResetRef.current = false;
});
<AnalyticalTable
data={data}
columns={columns}
retainColumnWidth // prevent column width reset on container resize
reactTableOptions={{
autoResetSelectedRows: !skipPageResetRef.current,
autoResetSortBy: !skipPageResetRef.current,
autoResetFilters: !skipPageResetRef.current,
autoResetGroupBy: !skipPageResetRef.current,
autoResetExpanded: !skipPageResetRef.current,
autoResetHiddenColumns: !skipPageResetRef.current,
}}
/>;
```
#### Row Selection with Active Elements
UI5 Web Components (Button, Link, Input, etc.) block row selection by default. To allow selection when clicking these elements:
```tsx
<Link
onClick={(e) => {
e.markerAllowTableRowSelection = true;
}}
>
My Link Text
</Link>
```
For other elements, call `event.stopPropagation()` or check the target.
#### Distinguishing onRowClick vs onRowSelect
When clicking a selectable cell, both events fire. Check for the selection cell:
```tsx
const onRowClick = (e) => {
if (e.target.dataset.selectionCell === 'true') {
// selection cell clicked
} else {
// other cell clicked
}
};
```
#### Subcomponents with Active Elements
Add `data-subcomponent-active-element` attribute for consistent focus behavior:
```tsx
const renderRowSubComponent = (row) => (
<div>
<Button data-subcomponent-active-element>Click</Button>
</div>
);
```
#### Server-side Sorting/Filtering/Grouping
```tsx
<AnalyticalTable
reactTableOptions={{
manualSortBy: true,
manualFilters: true,
manualGroupBy: true,
}}
/>
```
#### Custom Cell Ellipsis
For custom `Cell` renderers returning React elements, use `webComponentsReactProperties.classes.textEllipsis`:
```tsx
Cell: ({ value, webComponentsReactProperties }) => (
<span className={webComponentsReactProperties.classes.textEllipsis} title={value}>
{value}
</span>
);
```
#### Dialogs/Popovers in Custom Cells
Mount modals outside the table when possible. If inside a cell:
- Use conditional rendering
- Call `event.stopPropagation()` for focus/keyboard events:
```tsx
<Dialog
onFocus={(e) => e.stopPropagation()}
>
```
#### Inputs in Cells - Arrow Key Navigation
Stop arrow key propagation to allow caret movement:
```tsx
<Input
onKeyDown={(e) => {
if (e.key.startsWith('Arrow')) {
e.stopPropagation();
}
}}
/>
```
#### Scrolling Methods
Available on DOM ref:
- `scrollTo(offset, align?)` - Scroll to pixel offset
- `scrollToItem(index, align?)` - Scroll to row index
- `horizontalScrollTo(offset, align?)`
- `horizontalScrollToItem(index, align?)`
---
### ObjectPage
**Limitation:** In iframes, smooth scrolling is disabled.
**Toggling header programmatically:**
```tsx
const objectPageRef = useRef<ObjectPageDomRef>(null);
objectPageRef.current.toggleHeaderArea(); // toggle
objectPageRef.current.toggleHeaderArea(true); // snap
objectPageRef.current.toggleHeaderArea(false); // expand
```
**Fullscreen section (TabBar mode only):**
```tsx
<ObjectPageSection titleText="Section" id="section" style={{ height: '100%', overflow: 'auto' }}>
{/* Content */}
</ObjectPageSection>
```
**Note:** Using multiple sections with `height: 100%` will break your layout.
---
### FilterBar
#### Identifying Event Origin
When filters should only update from FilterBar (not FiltersDialog), check the dataset:
```tsx
const handleInput = (e) => {
const firedInFilterBar = !!e.currentTarget.parentElement.dataset.inFilterBar;
const firedInFiltersDialog = !!e.currentTarget.parentElement.dataset.inFiltersDialog;
};
```
#### Reordering
Enable with `enableReordering={true}`. Handle `onFiltersDialogSave` to persist order:
```tsx
const handleFiltersDialogSave = (e) => {
setOrderedFilterKeys(e.detail.reorderedFilterKeys);
setVisibleFilters(e.detail.selectedFilterKeys);
};
```
---
### VariantManagement
#### Custom Input Validation
Prevent internal save logic by marking the event as invalid:
```tsx
const handleSaveViewInput = (e) => {
if (!e.target.value.match(/^[a-z0-9\s]+$/i)) {
e.isInvalid = true; // prevents save
setValueState(ValueState.Negative);
} else {
e.isInvalid = false;
setValueState(undefined);
}
};
<VariantItem
saveViewInputProps={{
valueState: valueState,
valueStateMessage: <div>Error message</div>,
onInput: handleSaveViewInput,
}}
>
Variant Name
</VariantItem>;
```
---
### MessageView
**Navigation pattern in Dialog/Popover:**
Don't set `showDetailsPageHeader`. Instead, listen to `onItemSelect` and use `navigateBack()`:
```tsx
const messageViewRef = useRef<MessageViewDomRef>(null);
const [isOnDetailsPage, setIsOnDetailsPage] = useState(false);
<Dialog
onClose={() => messageViewRef.current.navigateBack()}
header={
<Bar
startContent={
isOnDetailsPage && (
<Button
icon="slim-arrow-left"
onClick={() => {
setIsOnDetailsPage(false);
messageViewRef.current.navigateBack();
}}
/>
)
}
/>
}
>
<MessageView ref={messageViewRef} showDetailsPageHeader={false} onItemSelect={() => setIsOnDetailsPage(true)} />
</Dialog>;
```
---
### SplitterLayout
**Reset options via `options` prop:**
| Property | Description |
| ------------------------- | --------------------------------------- |
| `resetOnSizeChange` | Reset when container size changes |
| `resetOnChildrenChange` | Reset when children change |
| `resetOnCustomDepsChange` | Custom dependency list to trigger reset |
---
## Controlled Components (preventDefault)
UI5 Web Components update internal state **before** firing events. For fully controlled components, call `e.preventDefault()` in the event handler.
**Cancelable events:** `Input/TextArea.onInput`, `CheckBox.onChange`, `Select.onChange`, `DatePicker.onChange/onInput/onValueStateChange`, `DateRangePicker/DateTimePicker.onChange/onInput/onValueStateChange`, `Dialog/Popover/ResponsivePopover.onBeforeClose/onBeforeOpen`, `Button.onClick`, `Link.onClick`, `TabContainer.onTabSelect/onMoveOver`, `List.onItemClick/onMoveOver`, `Calendar.onSelectionChange`, `Breadcrumbs.onItemClick`, `DynamicDateRange.onChange`, `Tree.onItemToggle/onMove`