UNPKG

@ui5/webcomponents-react

Version:

React Wrapper for UI5 Web Components and additional components

697 lines (520 loc) 21.7 kB
# @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`