This is page 174 of 182. Use http://codebase.md/xmlui-org/xmlui/cantFindIt.jpg?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .changeset │ ├── config.json │ ├── cool-queens-look.md │ ├── hot-berries-argue.md │ ├── twelve-guests-care.md │ └── wise-towns-dance.md ├── .eslintrc.cjs ├── .github │ ├── build-checklist.png │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows │ ├── deploy-blog.yml │ ├── deploy-docs-optimized.yml │ ├── deploy-docs.yml │ ├── prepare-versions.yml │ ├── release-packages.yml │ ├── run-all-tests.yml │ └── run-smoke-tests.yml ├── .gitignore ├── .prettierrc.js ├── .vscode │ ├── launch.json │ └── settings.json ├── blog │ ├── .gitignore │ ├── .gitkeep │ ├── CHANGELOG.md │ ├── extensions.ts │ ├── index.html │ ├── index.ts │ ├── package.json │ ├── public │ │ ├── blog │ │ │ ├── images │ │ │ │ ├── blog-page-component.png │ │ │ │ ├── blog-scrabble.png │ │ │ │ ├── integrated-blog-search.png │ │ │ │ └── lorem-ipsum.png │ │ │ ├── lorem-ipsum.md │ │ │ ├── newest-post.md │ │ │ ├── older-post.md │ │ │ └── welcome-to-the-xmlui-blog.md │ │ ├── mockServiceWorker.js │ │ ├── netlify.toml │ │ ├── resources │ │ │ ├── favicon.ico │ │ │ ├── files │ │ │ │ └── for-download │ │ │ │ └── xmlui │ │ │ │ └── xmlui-standalone.umd.js │ │ │ ├── github.svg │ │ │ ├── llms.txt │ │ │ ├── logo-dark.svg │ │ │ ├── logo.svg │ │ │ ├── pg-popout.svg │ │ │ └── xmlui-logo.svg │ │ ├── serve.json │ │ └── web.config │ ├── scripts │ │ ├── download-latest-xmlui.js │ │ ├── generate-rss.js │ │ ├── get-releases.js │ │ └── utils.js │ ├── src │ │ ├── components │ │ │ ├── BlogOverview.xmlui │ │ │ ├── BlogPage.xmlui │ │ │ ├── Debug.xmlui │ │ │ └── PageNotFound.xmlui │ │ ├── config.ts │ │ ├── Main.xmlui │ │ ├── Main.xmlui.xs │ │ └── themes │ │ ├── docs-theme.ts │ │ ├── earthtone.ts │ │ ├── xmlui-gray-on-default.ts │ │ ├── xmlui-green-on-default.ts │ │ └── xmlui-orange-on-default.ts │ └── tsconfig.json ├── CONTRIBUTING.md ├── docs │ ├── .gitignore │ ├── CHANGELOG.md │ ├── ComponentRefLinks.txt │ ├── content │ │ ├── _meta.json │ │ ├── components │ │ │ ├── _meta.json │ │ │ ├── _overview.md │ │ │ ├── APICall.md │ │ │ ├── App.md │ │ │ ├── AppHeader.md │ │ │ ├── AppState.md │ │ │ ├── AutoComplete.md │ │ │ ├── Avatar.md │ │ │ ├── Backdrop.md │ │ │ ├── Badge.md │ │ │ ├── BarChart.md │ │ │ ├── Bookmark.md │ │ │ ├── Breakout.md │ │ │ ├── Button.md │ │ │ ├── Card.md │ │ │ ├── Carousel.md │ │ │ ├── ChangeListener.md │ │ │ ├── Checkbox.md │ │ │ ├── CHStack.md │ │ │ ├── ColorPicker.md │ │ │ ├── Column.md │ │ │ ├── ContentSeparator.md │ │ │ ├── CVStack.md │ │ │ ├── DataSource.md │ │ │ ├── DateInput.md │ │ │ ├── DatePicker.md │ │ │ ├── DonutChart.md │ │ │ ├── DropdownMenu.md │ │ │ ├── EmojiSelector.md │ │ │ ├── ExpandableItem.md │ │ │ ├── FileInput.md │ │ │ ├── FileUploadDropZone.md │ │ │ ├── FlowLayout.md │ │ │ ├── Footer.md │ │ │ ├── Form.md │ │ │ ├── FormItem.md │ │ │ ├── FormSection.md │ │ │ ├── Fragment.md │ │ │ ├── H1.md │ │ │ ├── H2.md │ │ │ ├── H3.md │ │ │ ├── H4.md │ │ │ ├── H5.md │ │ │ ├── H6.md │ │ │ ├── Heading.md │ │ │ ├── HSplitter.md │ │ │ ├── HStack.md │ │ │ ├── Icon.md │ │ │ ├── IFrame.md │ │ │ ├── Image.md │ │ │ ├── Items.md │ │ │ ├── LabelList.md │ │ │ ├── Legend.md │ │ │ ├── LineChart.md │ │ │ ├── Link.md │ │ │ ├── List.md │ │ │ ├── Logo.md │ │ │ ├── Markdown.md │ │ │ ├── MenuItem.md │ │ │ ├── MenuSeparator.md │ │ │ ├── ModalDialog.md │ │ │ ├── NavGroup.md │ │ │ ├── NavLink.md │ │ │ ├── NavPanel.md │ │ │ ├── NoResult.md │ │ │ ├── NumberBox.md │ │ │ ├── Option.md │ │ │ ├── Page.md │ │ │ ├── PageMetaTitle.md │ │ │ ├── Pages.md │ │ │ ├── Pagination.md │ │ │ ├── PasswordInput.md │ │ │ ├── PieChart.md │ │ │ ├── ProgressBar.md │ │ │ ├── Queue.md │ │ │ ├── RadioGroup.md │ │ │ ├── RealTimeAdapter.md │ │ │ ├── Redirect.md │ │ │ ├── Select.md │ │ │ ├── Slider.md │ │ │ ├── Slot.md │ │ │ ├── SpaceFiller.md │ │ │ ├── Spinner.md │ │ │ ├── Splitter.md │ │ │ ├── Stack.md │ │ │ ├── StickyBox.md │ │ │ ├── SubMenuItem.md │ │ │ ├── Switch.md │ │ │ ├── TabItem.md │ │ │ ├── Table.md │ │ │ ├── TableOfContents.md │ │ │ ├── Tabs.md │ │ │ ├── Text.md │ │ │ ├── TextArea.md │ │ │ ├── TextBox.md │ │ │ ├── Theme.md │ │ │ ├── TimeInput.md │ │ │ ├── Timer.md │ │ │ ├── ToneChangerButton.md │ │ │ ├── ToneSwitch.md │ │ │ ├── Tooltip.md │ │ │ ├── Tree.md │ │ │ ├── VSplitter.md │ │ │ ├── VStack.md │ │ │ ├── xmlui-animations │ │ │ │ ├── _meta.json │ │ │ │ ├── _overview.md │ │ │ │ ├── Animation.md │ │ │ │ ├── FadeAnimation.md │ │ │ │ ├── FadeInAnimation.md │ │ │ │ ├── FadeOutAnimation.md │ │ │ │ ├── ScaleAnimation.md │ │ │ │ └── SlideInAnimation.md │ │ │ ├── xmlui-pdf │ │ │ │ ├── _meta.json │ │ │ │ ├── _overview.md │ │ │ │ └── Pdf.md │ │ │ ├── xmlui-spreadsheet │ │ │ │ ├── _meta.json │ │ │ │ ├── _overview.md │ │ │ │ └── Spreadsheet.md │ │ │ └── xmlui-website-blocks │ │ │ ├── _meta.json │ │ │ ├── _overview.md │ │ │ ├── Carousel.md │ │ │ ├── HelloMd.md │ │ │ ├── HeroSection.md │ │ │ └── ScrollToTop.md │ │ └── extensions │ │ ├── _meta.json │ │ ├── xmlui-animations │ │ │ ├── _meta.json │ │ │ ├── _overview.md │ │ │ ├── Animation.md │ │ │ ├── FadeAnimation.md │ │ │ ├── FadeInAnimation.md │ │ │ ├── FadeOutAnimation.md │ │ │ ├── ScaleAnimation.md │ │ │ └── SlideInAnimation.md │ │ └── xmlui-website-blocks │ │ ├── _meta.json │ │ ├── _overview.md │ │ ├── Carousel.md │ │ ├── HelloMd.md │ │ ├── HeroSection.md │ │ └── ScrollToTop.md │ ├── extensions.ts │ ├── index.html │ ├── index.ts │ ├── package.json │ ├── public │ │ ├── feed.rss │ │ ├── mockServiceWorker.js │ │ ├── pages │ │ │ ├── _meta.json │ │ │ ├── app-structure.md │ │ │ ├── build-editor-component.md │ │ │ ├── build-hello-world-component.md │ │ │ ├── components-intro.md │ │ │ ├── context-variables.md │ │ │ ├── forms.md │ │ │ ├── globals.md │ │ │ ├── glossary.md │ │ │ ├── helper-tags.md │ │ │ ├── hosted-deployment.md │ │ │ ├── howto │ │ │ │ ├── assign-a-complex-json-literal-to-a-component-variable.md │ │ │ │ ├── chain-a-refetch.md │ │ │ │ ├── debug-a-component.md │ │ │ │ ├── delay-a-datasource-until-another-datasource-is-ready.md │ │ │ │ ├── delegate-a-method.md │ │ │ │ ├── do-custom-form-validation.md │ │ │ │ ├── expose-a-method-from-a-component.md │ │ │ │ ├── filter-and-transform-data-from-an-api.md │ │ │ │ ├── group-items-in-list-by-a-property.md │ │ │ │ ├── handle-background-operations.md │ │ │ │ ├── hide-an-element-until-its-datasource-is-ready.md │ │ │ │ ├── make-a-set-of-equal-width-cards.md │ │ │ │ ├── make-a-table-responsive.md │ │ │ │ ├── make-navpanel-width-responsive.md │ │ │ │ ├── modify-a-value-reported-in-a-column.md │ │ │ │ ├── paginate-a-list.md │ │ │ │ ├── pass-data-to-a-modal-dialog.md │ │ │ │ ├── react-to-button-click-not-keystrokes.md │ │ │ │ ├── set-the-initial-value-of-a-select-from-fetched-data.md │ │ │ │ ├── share-a-modaldialog-across-components.md │ │ │ │ ├── sync-selections-between-table-and-list-views.md │ │ │ │ ├── update-ui-optimistically.md │ │ │ │ ├── use-built-in-form-validation.md │ │ │ │ └── use-the-same-modaldialog-to-add-or-edit.md │ │ │ ├── howto.md │ │ │ ├── intro.md │ │ │ ├── layout.md │ │ │ ├── markup.md │ │ │ ├── mcp.md │ │ │ ├── modal-dialogs.md │ │ │ ├── news-and-reviews.md │ │ │ ├── reactive-intro.md │ │ │ ├── refactoring.md │ │ │ ├── routing-and-links.md │ │ │ ├── samples │ │ │ │ ├── color-palette.xmlui │ │ │ │ ├── color-values.xmlui │ │ │ │ ├── shadow-sizes.xmlui │ │ │ │ ├── spacing-sizes.xmlui │ │ │ │ ├── swatch.xmlui │ │ │ │ ├── theme-gallery-brief.xmlui │ │ │ │ └── theme-gallery.xmlui │ │ │ ├── scoping.md │ │ │ ├── scripting.md │ │ │ ├── styles-and-themes │ │ │ │ ├── common-units.md │ │ │ │ ├── layout-props.md │ │ │ │ ├── theme-variable-defaults.md │ │ │ │ ├── theme-variables.md │ │ │ │ └── themes.md │ │ │ ├── template-properties.md │ │ │ ├── test.md │ │ │ ├── tutorial-01.md │ │ │ ├── tutorial-02.md │ │ │ ├── tutorial-03.md │ │ │ ├── tutorial-04.md │ │ │ ├── tutorial-05.md │ │ │ ├── tutorial-06.md │ │ │ ├── tutorial-07.md │ │ │ ├── tutorial-08.md │ │ │ ├── tutorial-09.md │ │ │ ├── tutorial-10.md │ │ │ ├── tutorial-11.md │ │ │ ├── tutorial-12.md │ │ │ ├── universal-properties.md │ │ │ ├── user-defined-components.md │ │ │ ├── vscode.md │ │ │ ├── working-with-markdown.md │ │ │ ├── working-with-text.md │ │ │ ├── xmlui-animations │ │ │ │ ├── _meta.json │ │ │ │ ├── _overview.md │ │ │ │ ├── Animation.md │ │ │ │ ├── FadeAnimation.md │ │ │ │ ├── FadeInAnimation.md │ │ │ │ ├── FadeOutAnimation.md │ │ │ │ ├── ScaleAnimation.md │ │ │ │ └── SlideInAnimation.md │ │ │ ├── xmlui-charts │ │ │ │ ├── _meta.json │ │ │ │ ├── _overview.md │ │ │ │ ├── BarChart.md │ │ │ │ ├── DonutChart.md │ │ │ │ ├── LabelList.md │ │ │ │ ├── Legend.md │ │ │ │ ├── LineChart.md │ │ │ │ └── PieChart.md │ │ │ ├── xmlui-pdf │ │ │ │ ├── _meta.json │ │ │ │ ├── _overview.md │ │ │ │ └── Pdf.md │ │ │ └── xmlui-spreadsheet │ │ │ ├── _meta.json │ │ │ ├── _overview.md │ │ │ └── Spreadsheet.md │ │ ├── resources │ │ │ ├── devdocs │ │ │ │ ├── debug-proxy-object-2.png │ │ │ │ ├── debug-proxy-object.png │ │ │ │ ├── table_editor_01.png │ │ │ │ ├── table_editor_02.png │ │ │ │ ├── table_editor_03.png │ │ │ │ ├── table_editor_04.png │ │ │ │ ├── table_editor_05.png │ │ │ │ ├── table_editor_06.png │ │ │ │ ├── table_editor_07.png │ │ │ │ ├── table_editor_08.png │ │ │ │ ├── table_editor_09.png │ │ │ │ ├── table_editor_10.png │ │ │ │ ├── table_editor_11.png │ │ │ │ ├── table-editor-01.png │ │ │ │ ├── table-editor-02.png │ │ │ │ ├── table-editor-03.png │ │ │ │ ├── table-editor-04.png │ │ │ │ ├── table-editor-06.png │ │ │ │ ├── table-editor-07.png │ │ │ │ ├── table-editor-08.png │ │ │ │ ├── table-editor-09.png │ │ │ │ └── xmlui-rendering-of-tiptap-markdown.png │ │ │ ├── favicon.ico │ │ │ ├── files │ │ │ │ ├── clients.json │ │ │ │ ├── daily-revenue.json │ │ │ │ ├── dashboard-stats.json │ │ │ │ ├── demo.xmlui │ │ │ │ ├── demo.xmlui.xs │ │ │ │ ├── downloads │ │ │ │ │ └── downloads.json │ │ │ │ ├── for-download │ │ │ │ │ ├── index-with-api.html │ │ │ │ │ ├── index.html │ │ │ │ │ ├── mockApi.js │ │ │ │ │ ├── start-darwin.sh │ │ │ │ │ ├── start-linux.sh │ │ │ │ │ ├── start.bat │ │ │ │ │ └── xmlui │ │ │ │ │ └── xmlui-standalone.umd.js │ │ │ │ ├── getting-started │ │ │ │ │ ├── cl-tutorial-final.zip │ │ │ │ │ ├── cl-tutorial.zip │ │ │ │ │ ├── cl-tutorial2.zip │ │ │ │ │ ├── cl-tutorial3.zip │ │ │ │ │ ├── cl-tutorial4.zip │ │ │ │ │ ├── cl-tutorial5.zip │ │ │ │ │ ├── cl-tutorial6.zip │ │ │ │ │ ├── getting-started.zip │ │ │ │ │ ├── hello-xmlui.zip │ │ │ │ │ ├── xmlui-empty.zip │ │ │ │ │ └── xmlui-starter.zip │ │ │ │ ├── howto │ │ │ │ │ └── component-icons │ │ │ │ │ └── up-arrow.svg │ │ │ │ ├── invoices.json │ │ │ │ ├── monthly-status.json │ │ │ │ ├── news-and-reviews.json │ │ │ │ ├── products.json │ │ │ │ ├── releases.json │ │ │ │ ├── tutorials │ │ │ │ │ ├── datasource │ │ │ │ │ │ └── api.ts │ │ │ │ │ └── p2do │ │ │ │ │ ├── api.ts │ │ │ │ │ └── todo-logo.svg │ │ │ │ └── xmlui.json │ │ │ ├── github.svg │ │ │ ├── images │ │ │ │ ├── apiaction-tutorial │ │ │ │ │ ├── add-success.png │ │ │ │ │ ├── apiaction-param.png │ │ │ │ │ ├── change-completed.png │ │ │ │ │ ├── change-in-progress.png │ │ │ │ │ ├── confirm-delete.png │ │ │ │ │ ├── data-error.png │ │ │ │ │ ├── data-progress.png │ │ │ │ │ ├── data-success.png │ │ │ │ │ ├── display-1.png │ │ │ │ │ ├── item-deleted.png │ │ │ │ │ ├── item-updated.png │ │ │ │ │ ├── missing-api-key.png │ │ │ │ │ ├── new-item-added.png │ │ │ │ │ └── test-message.png │ │ │ │ ├── chat-api │ │ │ │ │ └── domain-model.svg │ │ │ │ ├── components │ │ │ │ │ ├── image │ │ │ │ │ │ └── breakfast.jpg │ │ │ │ │ ├── markdown │ │ │ │ │ │ └── colors.png │ │ │ │ │ └── modal │ │ │ │ │ ├── deep_link_dialog_1.jpg │ │ │ │ │ └── deep_link_dialog_2.jpg │ │ │ │ ├── create-apps │ │ │ │ │ ├── collapsed-vertical.png │ │ │ │ │ ├── using-forms-warning-dialog.png │ │ │ │ │ └── using-forms.png │ │ │ │ ├── datasource-tutorial │ │ │ │ │ ├── data-with-header.png │ │ │ │ │ ├── filtered-data.png │ │ │ │ │ ├── filtered-items.png │ │ │ │ │ ├── initial-page-items.png │ │ │ │ │ ├── list-items.png │ │ │ │ │ ├── next-page-items.png │ │ │ │ │ ├── no-data.png │ │ │ │ │ ├── pagination-1.jpg │ │ │ │ │ ├── pagination-1.png │ │ │ │ │ ├── polling-1.png │ │ │ │ │ ├── refetch-data.png │ │ │ │ │ ├── slow-loading.png │ │ │ │ │ ├── test-message.png │ │ │ │ │ ├── Thumbs.db │ │ │ │ │ ├── unconventional-data.png │ │ │ │ │ └── unfiltered-items.png │ │ │ │ ├── flower.jpg │ │ │ │ ├── get-started │ │ │ │ │ ├── add-new-contact.png │ │ │ │ │ ├── app-modified.png │ │ │ │ │ ├── app-start.png │ │ │ │ │ ├── app-with-boxes.png │ │ │ │ │ ├── app-with-toast.png │ │ │ │ │ ├── boilerplate-structure.png │ │ │ │ │ ├── cl-initial.png │ │ │ │ │ ├── cl-start.png │ │ │ │ │ ├── contact-counts.png │ │ │ │ │ ├── contact-dialog-title.png │ │ │ │ │ ├── contact-dialog.png │ │ │ │ │ ├── contact-menus.png │ │ │ │ │ ├── contact-predicates.png │ │ │ │ │ ├── context-menu.png │ │ │ │ │ ├── dashboard-numbers.png │ │ │ │ │ ├── default-contact-list.png │ │ │ │ │ ├── delete-contact.png │ │ │ │ │ ├── delete-task.png │ │ │ │ │ ├── detailed-template.png │ │ │ │ │ ├── edit-contact-details.png │ │ │ │ │ ├── edited-contact-saved.png │ │ │ │ │ ├── empty-sections.png │ │ │ │ │ ├── filter-completed.png │ │ │ │ │ ├── fullwidth-desktop.png │ │ │ │ │ ├── fullwidth-mobile.png │ │ │ │ │ ├── initial-table.png │ │ │ │ │ ├── items-and-badges.png │ │ │ │ │ ├── loading-message.png │ │ │ │ │ ├── new-contact-button.png │ │ │ │ │ ├── new-contact-saved.png │ │ │ │ │ ├── no-empty-sections.png │ │ │ │ │ ├── personal-todo-initial.png │ │ │ │ │ ├── piechart.png │ │ │ │ │ ├── review-today.png │ │ │ │ │ ├── rudimentary-dashboard.png │ │ │ │ │ ├── section-collapsed.png │ │ │ │ │ ├── sectioned-items.png │ │ │ │ │ ├── sections-ordered.png │ │ │ │ │ ├── spacex-list-with-links.png │ │ │ │ │ ├── spacex-list.png │ │ │ │ │ ├── start-personal-todo-1.png │ │ │ │ │ ├── submit-new-contact.png │ │ │ │ │ ├── submit-new-task.png │ │ │ │ │ ├── syntax-highlighting.png │ │ │ │ │ ├── table-with-badge.png │ │ │ │ │ ├── template-with-card.png │ │ │ │ │ ├── test-emulated-api.png │ │ │ │ │ ├── Thumbs.db │ │ │ │ │ ├── todo-logo.png │ │ │ │ │ └── xmlui-tools.png │ │ │ │ ├── HelloApp.png │ │ │ │ ├── HelloApp2.png │ │ │ │ ├── logos │ │ │ │ │ ├── xmlui1.svg │ │ │ │ │ ├── xmlui2.svg │ │ │ │ │ ├── xmlui3.svg │ │ │ │ │ ├── xmlui4.svg │ │ │ │ │ ├── xmlui5.svg │ │ │ │ │ ├── xmlui6.svg │ │ │ │ │ └── xmlui7.svg │ │ │ │ ├── pdf │ │ │ │ │ └── dummy-pdf.jpg │ │ │ │ ├── rendering-engine │ │ │ │ │ ├── AppEngine-flow.svg │ │ │ │ │ ├── Component.svg │ │ │ │ │ ├── CompoundComponent.svg │ │ │ │ │ ├── RootComponent.svg │ │ │ │ │ └── tree-with-containers.svg │ │ │ │ ├── reviewers-guide │ │ │ │ │ ├── AppEngine-flow.svg │ │ │ │ │ └── incbutton-in-action.png │ │ │ │ ├── tools │ │ │ │ │ └── boilerplate-structure.png │ │ │ │ ├── try.svg │ │ │ │ ├── tutorial │ │ │ │ │ ├── app-chat-history.png │ │ │ │ │ ├── app-content-placeholder.png │ │ │ │ │ ├── app-header-and-content.png │ │ │ │ │ ├── app-links-channel-selected.png │ │ │ │ │ ├── app-links-click.png │ │ │ │ │ ├── app-navigation.png │ │ │ │ │ ├── finished-ex01.png │ │ │ │ │ ├── finished-ex02.png │ │ │ │ │ ├── hello.png │ │ │ │ │ ├── splash-screen-advanced.png │ │ │ │ │ ├── splash-screen-after-click.png │ │ │ │ │ ├── splash-screen-centered.png │ │ │ │ │ ├── splash-screen-events.png │ │ │ │ │ ├── splash-screen-expression.png │ │ │ │ │ ├── splash-screen-reuse-after.png │ │ │ │ │ ├── splash-screen-reuse-before.png │ │ │ │ │ └── splash-screen.png │ │ │ │ └── tutorial-01.png │ │ │ ├── llms.txt │ │ │ ├── logo-dark.svg │ │ │ ├── logo.svg │ │ │ ├── pg-popout.svg │ │ │ └── xmlui-logo.svg │ │ ├── serve.json │ │ └── web.config │ ├── scripts │ │ ├── download-latest-xmlui.js │ │ ├── generate-rss.js │ │ ├── get-releases.js │ │ └── utils.js │ ├── src │ │ ├── components │ │ │ ├── BlogOverview.xmlui │ │ │ ├── BlogPage.xmlui │ │ │ ├── Boxes.xmlui │ │ │ ├── Breadcrumb.xmlui │ │ │ ├── ChangeLog.xmlui │ │ │ ├── ColorPalette.xmlui │ │ │ ├── DocumentLinks.xmlui │ │ │ ├── DocumentPage.xmlui │ │ │ ├── DocumentPageNoTOC.xmlui │ │ │ ├── Icons.xmlui │ │ │ ├── IncButton.xmlui │ │ │ ├── IncButton2.xmlui │ │ │ ├── NameValue.xmlui │ │ │ ├── PageNotFound.xmlui │ │ │ ├── PaletteItem.xmlui │ │ │ ├── Palettes.xmlui │ │ │ ├── SectionHeader.xmlui │ │ │ ├── TBD.xmlui │ │ │ ├── Test.xmlui │ │ │ ├── ThemesIntro.xmlui │ │ │ ├── ThousandThemes.xmlui │ │ │ ├── TubeStops.xmlui │ │ │ ├── TubeStops.xmlui.xs │ │ │ └── TwoColumnCode.xmlui │ │ ├── config.ts │ │ ├── Main.xmlui │ │ └── themes │ │ ├── docs-theme.ts │ │ ├── earthtone.ts │ │ ├── xmlui-gray-on-default.ts │ │ ├── xmlui-green-on-default.ts │ │ └── xmlui-orange-on-default.ts │ └── tsconfig.json ├── LICENSE ├── package-lock.json ├── package.json ├── packages │ ├── xmlui-animations │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── Animation.tsx │ │ │ ├── AnimationNative.tsx │ │ │ ├── FadeAnimation.tsx │ │ │ ├── FadeInAnimation.tsx │ │ │ ├── FadeOutAnimation.tsx │ │ │ ├── index.tsx │ │ │ ├── ScaleAnimation.tsx │ │ │ └── SlideInAnimation.tsx │ │ └── tsconfig.json │ ├── xmlui-devtools │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── devtools │ │ │ │ ├── DevTools.tsx │ │ │ │ ├── DevToolsNative.module.scss │ │ │ │ ├── DevToolsNative.tsx │ │ │ │ ├── ModalDialog.module.scss │ │ │ │ ├── ModalDialog.tsx │ │ │ │ ├── ModalVisibilityContext.tsx │ │ │ │ ├── Tooltip.module.scss │ │ │ │ ├── Tooltip.tsx │ │ │ │ └── utils.ts │ │ │ ├── editor │ │ │ │ └── Editor.tsx │ │ │ └── index.tsx │ │ ├── tsconfig.json │ │ └── vite.config-overrides.ts │ ├── xmlui-hello-world │ │ ├── .gitignore │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── HelloWorld.module.scss │ │ │ ├── HelloWorld.tsx │ │ │ ├── HelloWorldNative.tsx │ │ │ └── index.tsx │ │ └── tsconfig.json │ ├── xmlui-os-frames │ │ ├── .gitignore │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── index.tsx │ │ │ ├── IPhoneFrame.module.scss │ │ │ ├── IPhoneFrame.tsx │ │ │ ├── MacOSAppFrame.module.scss │ │ │ ├── MacOSAppFrame.tsx │ │ │ ├── WindowsAppFrame.module.scss │ │ │ └── WindowsAppFrame.tsx │ │ └── tsconfig.json │ ├── xmlui-pdf │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── demo │ │ │ ├── components │ │ │ │ └── Pdf.xmlui │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── index.tsx │ │ │ ├── LazyPdfNative.tsx │ │ │ ├── Pdf.module.scss │ │ │ └── Pdf.tsx │ │ └── tsconfig.json │ ├── xmlui-playground │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── hooks │ │ │ │ ├── usePlayground.ts │ │ │ │ └── useToast.ts │ │ │ ├── index.tsx │ │ │ ├── playground │ │ │ │ ├── Box.module.scss │ │ │ │ ├── Box.tsx │ │ │ │ ├── CodeSelector.tsx │ │ │ │ ├── ConfirmationDialog.module.scss │ │ │ │ ├── ConfirmationDialog.tsx │ │ │ │ ├── Editor.tsx │ │ │ │ ├── Header.module.scss │ │ │ │ ├── Header.tsx │ │ │ │ ├── Playground.tsx │ │ │ │ ├── PlaygroundContent.module.scss │ │ │ │ ├── PlaygroundContent.tsx │ │ │ │ ├── PlaygroundNative.module.scss │ │ │ │ ├── PlaygroundNative.tsx │ │ │ │ ├── Preview.module.scss │ │ │ │ ├── Preview.tsx │ │ │ │ ├── Select.module.scss │ │ │ │ ├── StandalonePlayground.tsx │ │ │ │ ├── StandalonePlaygroundNative.module.scss │ │ │ │ ├── StandalonePlaygroundNative.tsx │ │ │ │ ├── ThemeSwitcher.module.scss │ │ │ │ ├── ThemeSwitcher.tsx │ │ │ │ ├── ToneSwitcher.tsx │ │ │ │ ├── Tooltip.module.scss │ │ │ │ ├── Tooltip.tsx │ │ │ │ └── utils.ts │ │ │ ├── providers │ │ │ │ ├── Toast.module.scss │ │ │ │ └── ToastProvider.tsx │ │ │ ├── state │ │ │ │ └── store.ts │ │ │ ├── themes │ │ │ │ └── theme.ts │ │ │ └── utils │ │ │ └── helpers.ts │ │ └── tsconfig.json │ ├── xmlui-search │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── index.tsx │ │ │ ├── Search.module.scss │ │ │ └── Search.tsx │ │ └── tsconfig.json │ ├── xmlui-spreadsheet │ │ ├── .gitignore │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── index.tsx │ │ │ ├── Spreadsheet.tsx │ │ │ └── SpreadsheetNative.tsx │ │ └── tsconfig.json │ └── xmlui-website-blocks │ ├── .gitignore │ ├── CHANGELOG.md │ ├── demo │ │ ├── components │ │ │ ├── HeroBackgroundBreakoutPage.xmlui │ │ │ ├── HeroBackgroundsPage.xmlui │ │ │ ├── HeroContentsPage.xmlui │ │ │ ├── HeroTextAlignPage.xmlui │ │ │ ├── HeroTextPage.xmlui │ │ │ └── HeroTonesPage.xmlui │ │ ├── Main.xmlui │ │ └── themes │ │ └── default.ts │ ├── index.html │ ├── index.ts │ ├── meta │ │ └── componentsMetadata.ts │ ├── package.json │ ├── public │ │ └── resources │ │ ├── building.jpg │ │ └── xmlui-logo.svg │ ├── src │ │ ├── Carousel │ │ │ ├── Carousel.module.scss │ │ │ ├── Carousel.tsx │ │ │ ├── CarouselContext.tsx │ │ │ └── CarouselNative.tsx │ │ ├── FancyButton │ │ │ ├── FancyButton.module.scss │ │ │ ├── FancyButton.tsx │ │ │ └── FancyButton.xmlui │ │ ├── Hello │ │ │ ├── Hello.tsx │ │ │ ├── Hello.xmlui │ │ │ └── Hello.xmlui.xs │ │ ├── HeroSection │ │ │ ├── HeroSection.module.scss │ │ │ ├── HeroSection.tsx │ │ │ └── HeroSectionNative.tsx │ │ ├── index.tsx │ │ ├── ScrollToTop │ │ │ ├── ScrollToTop.module.scss │ │ │ ├── ScrollToTop.tsx │ │ │ └── ScrollToTopNative.tsx │ │ └── vite-env.d.ts │ └── tsconfig.json ├── README.md ├── tools │ ├── codefence │ │ └── xmlui-code-fence-docs.md │ ├── create-app │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── create-app.ts │ │ ├── helpers │ │ │ ├── copy.ts │ │ │ ├── get-pkg-manager.ts │ │ │ ├── git.ts │ │ │ ├── install.ts │ │ │ ├── is-folder-empty.ts │ │ │ ├── is-writeable.ts │ │ │ ├── make-dir.ts │ │ │ └── validate-pkg.ts │ │ ├── index.ts │ │ ├── package.json │ │ ├── templates │ │ │ ├── default │ │ │ │ └── ts │ │ │ │ ├── gitignore │ │ │ │ ├── index.html │ │ │ │ ├── index.ts │ │ │ │ ├── public │ │ │ │ │ ├── mockServiceWorker.js │ │ │ │ │ ├── resources │ │ │ │ │ │ ├── favicon.ico │ │ │ │ │ │ └── xmlui-logo.svg │ │ │ │ │ └── serve.json │ │ │ │ └── src │ │ │ │ ├── components │ │ │ │ │ ├── ApiAware.xmlui │ │ │ │ │ ├── Home.xmlui │ │ │ │ │ ├── IncButton.xmlui │ │ │ │ │ └── PagePanel.xmlui │ │ │ │ ├── config.ts │ │ │ │ └── Main.xmlui │ │ │ ├── index.ts │ │ │ └── types.ts │ │ └── tsconfig.json │ ├── create-xmlui-hello-world │ │ ├── index.js │ │ └── package.json │ └── vscode │ ├── .gitignore │ ├── .vscode │ │ ├── launch.json │ │ └── tasks.json │ ├── .vscodeignore │ ├── build.sh │ ├── CHANGELOG.md │ ├── esbuild.js │ ├── eslint.config.mjs │ ├── formatter-docs.md │ ├── generate-test-sample.sh │ ├── LICENSE.md │ ├── package-lock.json │ ├── package.json │ ├── README.md │ ├── resources │ │ ├── xmlui-logo.png │ │ └── xmlui-markup-syntax-highlighting.png │ ├── src │ │ ├── extension.ts │ │ └── server.ts │ ├── syntaxes │ │ └── xmlui.tmLanguage.json │ ├── test-samples │ │ └── sample.xmlui │ ├── tsconfig.json │ └── tsconfig.tsbuildinfo ├── turbo.json └── xmlui ├── .gitignore ├── bin │ ├── bootstrap.js │ ├── build-lib.ts │ ├── build.ts │ ├── index.ts │ ├── preview.ts │ ├── start.ts │ ├── vite-xmlui-plugin.ts │ └── viteConfig.ts ├── CHANGELOG.md ├── conventions │ ├── component-qa-checklist.md │ ├── copilot-conventions.md │ ├── create-xmlui-components.md │ ├── mermaid.md │ ├── testing-conventions.md │ └── xmlui-in-a-nutshell.md ├── dev-docs │ ├── accessibility.md │ ├── build-system.md │ ├── build-xmlui.md │ ├── component-behaviors.md │ ├── containers.md │ ├── glossary.md │ ├── index.md │ ├── next │ │ ├── component-dev-guide.md │ │ ├── configuration-management-enhancement-summary.md │ │ ├── documentation-scripts-refactoring-complete-summary.md │ │ ├── documentation-scripts-refactoring-plan.md │ │ ├── duplicate-pattern-extraction-summary.md │ │ ├── error-handling-standardization-summary.md │ │ ├── generating-component-reference.md │ │ ├── index.md │ │ ├── logging-consistency-implementation-summary.md │ │ ├── project-build.md │ │ ├── project-structure.md │ │ ├── theme-context.md │ │ ├── tiptap-design-considerations.md │ │ ├── working-with-code.md │ │ ├── xmlui-runtime-architecture │ │ └── xmlui-wcag-accessibility-report.md │ ├── react-fundamentals.md │ ├── release-method.md │ ├── standalone-app.md │ ├── state-management.md │ ├── ud-components.md │ └── xmlui-repo.md ├── package.json ├── playwright.config.ts ├── scripts │ ├── coverage-only.js │ ├── e2e-test-summary.js │ ├── generate-docs │ │ ├── build-downloads-map.mjs │ │ ├── build-pages-map.mjs │ │ ├── components-config.json │ │ ├── configuration-management.mjs │ │ ├── constants.mjs │ │ ├── create-theme-files.mjs │ │ ├── DocsGenerator.mjs │ │ ├── error-handling.mjs │ │ ├── extensions-config.json │ │ ├── folders.mjs │ │ ├── generate-summary-files.mjs │ │ ├── get-docs.mjs │ │ ├── input-handler.mjs │ │ ├── logger.mjs │ │ ├── logging-standards.mjs │ │ ├── MetadataProcessor.mjs │ │ ├── pattern-utilities.mjs │ │ └── utils.mjs │ ├── get-langserver-metadata.mjs │ ├── inline-links.mjs │ └── README-e2e-summary.md ├── src │ ├── abstractions │ │ ├── _conventions.md │ │ ├── ActionDefs.ts │ │ ├── AppContextDefs.ts │ │ ├── ComponentDefs.ts │ │ ├── ContainerDefs.ts │ │ ├── ExtensionDefs.ts │ │ ├── FunctionDefs.ts │ │ ├── RendererDefs.ts │ │ ├── scripting │ │ │ ├── BlockScope.ts │ │ │ ├── Compilation.ts │ │ │ ├── LogicalThread.ts │ │ │ ├── LoopScope.ts │ │ │ ├── modules.ts │ │ │ ├── ScriptParserError.ts │ │ │ ├── Token.ts │ │ │ ├── TryScope.ts │ │ │ └── TryScopeExp.ts │ │ └── ThemingDefs.ts │ ├── components │ │ ├── _conventions.md │ │ ├── abstractions.ts │ │ ├── Accordion │ │ │ ├── Accordion.md │ │ │ ├── Accordion.module.scss │ │ │ ├── Accordion.spec.ts │ │ │ ├── Accordion.tsx │ │ │ ├── AccordionContext.tsx │ │ │ ├── AccordionItem.tsx │ │ │ ├── AccordionItemNative.tsx │ │ │ └── AccordionNative.tsx │ │ ├── Animation │ │ │ └── AnimationNative.tsx │ │ ├── APICall │ │ │ ├── APICall.md │ │ │ ├── APICall.spec.ts │ │ │ ├── APICall.tsx │ │ │ └── APICallNative.tsx │ │ ├── App │ │ │ ├── App.md │ │ │ ├── App.module.scss │ │ │ ├── App.spec.ts │ │ │ ├── App.tsx │ │ │ ├── AppLayoutContext.ts │ │ │ ├── AppNative.tsx │ │ │ ├── AppStateContext.ts │ │ │ ├── doc-resources │ │ │ │ ├── condensed-sticky.xmlui │ │ │ │ ├── condensed.xmlui │ │ │ │ ├── horizontal-sticky.xmlui │ │ │ │ ├── horizontal.xmlui │ │ │ │ ├── vertical-full-header.xmlui │ │ │ │ ├── vertical-sticky.xmlui │ │ │ │ └── vertical.xmlui │ │ │ ├── IndexerContext.ts │ │ │ ├── LinkInfoContext.ts │ │ │ ├── SearchContext.tsx │ │ │ ├── Sheet.module.scss │ │ │ └── Sheet.tsx │ │ ├── AppHeader │ │ │ ├── AppHeader.md │ │ │ ├── AppHeader.module.scss │ │ │ ├── AppHeader.spec.ts │ │ │ ├── AppHeader.tsx │ │ │ └── AppHeaderNative.tsx │ │ ├── AppState │ │ │ ├── AppState.md │ │ │ ├── AppState.spec.ts │ │ │ ├── AppState.tsx │ │ │ └── AppStateNative.tsx │ │ ├── AutoComplete │ │ │ ├── AutoComplete.md │ │ │ ├── AutoComplete.module.scss │ │ │ ├── AutoComplete.spec.ts │ │ │ ├── AutoComplete.tsx │ │ │ ├── AutoCompleteContext.tsx │ │ │ └── AutoCompleteNative.tsx │ │ ├── Avatar │ │ │ ├── Avatar.md │ │ │ ├── Avatar.module.scss │ │ │ ├── Avatar.spec.ts │ │ │ ├── Avatar.tsx │ │ │ └── AvatarNative.tsx │ │ ├── Backdrop │ │ │ ├── Backdrop.md │ │ │ ├── Backdrop.module.scss │ │ │ ├── Backdrop.spec.ts │ │ │ ├── Backdrop.tsx │ │ │ └── BackdropNative.tsx │ │ ├── Badge │ │ │ ├── Badge.md │ │ │ ├── Badge.module.scss │ │ │ ├── Badge.spec.ts │ │ │ ├── Badge.tsx │ │ │ └── BadgeNative.tsx │ │ ├── Bookmark │ │ │ ├── Bookmark.md │ │ │ ├── Bookmark.module.scss │ │ │ ├── Bookmark.spec.ts │ │ │ ├── Bookmark.tsx │ │ │ └── BookmarkNative.tsx │ │ ├── Breakout │ │ │ ├── Breakout.module.scss │ │ │ ├── Breakout.spec.ts │ │ │ ├── Breakout.tsx │ │ │ └── BreakoutNative.tsx │ │ ├── Button │ │ │ ├── Button-style.spec.ts │ │ │ ├── Button.md │ │ │ ├── Button.module.scss │ │ │ ├── Button.spec.ts │ │ │ ├── Button.tsx │ │ │ └── ButtonNative.tsx │ │ ├── Card │ │ │ ├── Card.md │ │ │ ├── Card.module.scss │ │ │ ├── Card.spec.ts │ │ │ ├── Card.tsx │ │ │ └── CardNative.tsx │ │ ├── Carousel │ │ │ ├── Carousel.md │ │ │ ├── Carousel.module.scss │ │ │ ├── Carousel.spec.ts │ │ │ ├── Carousel.tsx │ │ │ ├── CarouselContext.tsx │ │ │ ├── CarouselItem.tsx │ │ │ ├── CarouselItemNative.tsx │ │ │ └── CarouselNative.tsx │ │ ├── ChangeListener │ │ │ ├── ChangeListener.md │ │ │ ├── ChangeListener.spec.ts │ │ │ ├── ChangeListener.tsx │ │ │ └── ChangeListenerNative.tsx │ │ ├── chart-color-schemes.ts │ │ ├── Charts │ │ │ ├── AreaChart │ │ │ │ ├── AreaChart.md │ │ │ │ ├── AreaChart.spec.ts │ │ │ │ ├── AreaChart.tsx │ │ │ │ └── AreaChartNative.tsx │ │ │ ├── BarChart │ │ │ │ ├── BarChart.md │ │ │ │ ├── BarChart.module.scss │ │ │ │ ├── BarChart.spec.ts │ │ │ │ ├── BarChart.tsx │ │ │ │ └── BarChartNative.tsx │ │ │ ├── DonutChart │ │ │ │ ├── DonutChart.spec.ts │ │ │ │ └── DonutChart.tsx │ │ │ ├── LabelList │ │ │ │ ├── LabelList.spec.ts │ │ │ │ ├── LabelList.tsx │ │ │ │ ├── LabelListNative.module.scss │ │ │ │ └── LabelListNative.tsx │ │ │ ├── Legend │ │ │ │ ├── Legend.spec.ts │ │ │ │ ├── Legend.tsx │ │ │ │ └── LegendNative.tsx │ │ │ ├── LineChart │ │ │ │ ├── LineChart.md │ │ │ │ ├── LineChart.module.scss │ │ │ │ ├── LineChart.spec.ts │ │ │ │ ├── LineChart.tsx │ │ │ │ └── LineChartNative.tsx │ │ │ ├── PieChart │ │ │ │ ├── PieChart.md │ │ │ │ ├── PieChart.spec.ts │ │ │ │ ├── PieChart.tsx │ │ │ │ ├── PieChartNative.module.scss │ │ │ │ └── PieChartNative.tsx │ │ │ ├── RadarChart │ │ │ │ ├── RadarChart.md │ │ │ │ ├── RadarChart.spec.ts │ │ │ │ ├── RadarChart.tsx │ │ │ │ └── RadarChartNative.tsx │ │ │ ├── Tooltip │ │ │ │ ├── TooltipContent.module.scss │ │ │ │ ├── TooltipContent.spec.ts │ │ │ │ └── TooltipContent.tsx │ │ │ └── utils │ │ │ ├── abstractions.ts │ │ │ └── ChartProvider.tsx │ │ ├── Checkbox │ │ │ ├── Checkbox.md │ │ │ ├── Checkbox.spec.ts │ │ │ └── Checkbox.tsx │ │ ├── CodeBlock │ │ │ ├── CodeBlock.module.scss │ │ │ ├── CodeBlock.spec.ts │ │ │ ├── CodeBlock.tsx │ │ │ ├── CodeBlockNative.tsx │ │ │ └── highlight-code.ts │ │ ├── collectedComponentMetadata.ts │ │ ├── ColorPicker │ │ │ ├── ColorPicker.md │ │ │ ├── ColorPicker.module.scss │ │ │ ├── ColorPicker.spec.ts │ │ │ ├── ColorPicker.tsx │ │ │ └── ColorPickerNative.tsx │ │ ├── Column │ │ │ ├── Column.md │ │ │ ├── Column.tsx │ │ │ ├── ColumnNative.tsx │ │ │ ├── doc-resources │ │ │ │ └── list-component-data.js │ │ │ └── TableContext.tsx │ │ ├── component-utils.ts │ │ ├── ComponentProvider.tsx │ │ ├── ComponentRegistryContext.tsx │ │ ├── container-helpers.tsx │ │ ├── ContentSeparator │ │ │ ├── ContentSeparator.md │ │ │ ├── ContentSeparator.module.scss │ │ │ ├── ContentSeparator.spec.ts │ │ │ ├── ContentSeparator.tsx │ │ │ └── ContentSeparatorNative.tsx │ │ ├── DataSource │ │ │ ├── DataSource.md │ │ │ └── DataSource.tsx │ │ ├── DateInput │ │ │ ├── DateInput.md │ │ │ ├── DateInput.module.scss │ │ │ ├── DateInput.spec.ts │ │ │ ├── DateInput.tsx │ │ │ └── DateInputNative.tsx │ │ ├── DatePicker │ │ │ ├── DatePicker.md │ │ │ ├── DatePicker.module.scss │ │ │ ├── DatePicker.spec.ts │ │ │ ├── DatePicker.tsx │ │ │ └── DatePickerNative.tsx │ │ ├── DropdownMenu │ │ │ ├── DropdownMenu.md │ │ │ ├── DropdownMenu.module.scss │ │ │ ├── DropdownMenu.spec.ts │ │ │ ├── DropdownMenu.tsx │ │ │ ├── DropdownMenuNative.tsx │ │ │ ├── MenuItem.md │ │ │ └── SubMenuItem.md │ │ ├── EmojiSelector │ │ │ ├── EmojiSelector.md │ │ │ ├── EmojiSelector.spec.ts │ │ │ ├── EmojiSelector.tsx │ │ │ └── EmojiSelectorNative.tsx │ │ ├── ExpandableItem │ │ │ ├── ExpandableItem.module.scss │ │ │ ├── ExpandableItem.spec.ts │ │ │ ├── ExpandableItem.tsx │ │ │ └── ExpandableItemNative.tsx │ │ ├── FileInput │ │ │ ├── FileInput.md │ │ │ ├── FileInput.module.scss │ │ │ ├── FileInput.spec.ts │ │ │ ├── FileInput.tsx │ │ │ └── FileInputNative.tsx │ │ ├── FileUploadDropZone │ │ │ ├── FileUploadDropZone.md │ │ │ ├── FileUploadDropZone.module.scss │ │ │ ├── FileUploadDropZone.spec.ts │ │ │ ├── FileUploadDropZone.tsx │ │ │ └── FileUploadDropZoneNative.tsx │ │ ├── FlowLayout │ │ │ ├── FlowLayout.md │ │ │ ├── FlowLayout.module.scss │ │ │ ├── FlowLayout.spec.ts │ │ │ ├── FlowLayout.spec.ts-snapshots │ │ │ │ └── Edge-cases-boxShadow-is-not-clipped-1-non-smoke-darwin.png │ │ │ ├── FlowLayout.tsx │ │ │ └── FlowLayoutNative.tsx │ │ ├── Footer │ │ │ ├── Footer.md │ │ │ ├── Footer.module.scss │ │ │ ├── Footer.spec.ts │ │ │ ├── Footer.tsx │ │ │ └── FooterNative.tsx │ │ ├── Form │ │ │ ├── Form.md │ │ │ ├── Form.module.scss │ │ │ ├── Form.spec.ts │ │ │ ├── Form.tsx │ │ │ ├── formActions.ts │ │ │ ├── FormContext.ts │ │ │ └── FormNative.tsx │ │ ├── FormItem │ │ │ ├── FormItem.md │ │ │ ├── FormItem.module.scss │ │ │ ├── FormItem.spec.ts │ │ │ ├── FormItem.tsx │ │ │ ├── FormItemNative.tsx │ │ │ ├── HelperText.module.scss │ │ │ ├── HelperText.tsx │ │ │ ├── ItemWithLabel.tsx │ │ │ └── Validations.ts │ │ ├── FormSection │ │ │ ├── FormSection.md │ │ │ ├── FormSection.ts │ │ │ └── FormSection.xmlui │ │ ├── Fragment │ │ │ ├── Fragment.spec.ts │ │ │ └── Fragment.tsx │ │ ├── Heading │ │ │ ├── abstractions.ts │ │ │ ├── H1.md │ │ │ ├── H1.spec.ts │ │ │ ├── H2.md │ │ │ ├── H2.spec.ts │ │ │ ├── H3.md │ │ │ ├── H3.spec.ts │ │ │ ├── H4.md │ │ │ ├── H4.spec.ts │ │ │ ├── H5.md │ │ │ ├── H5.spec.ts │ │ │ ├── H6.md │ │ │ ├── H6.spec.ts │ │ │ ├── Heading.md │ │ │ ├── Heading.module.scss │ │ │ ├── Heading.spec.ts │ │ │ ├── Heading.tsx │ │ │ └── HeadingNative.tsx │ │ ├── HoverCard │ │ │ ├── HoverCard.tsx │ │ │ └── HovercardNative.tsx │ │ ├── HtmlTags │ │ │ ├── HtmlTags.module.scss │ │ │ ├── HtmlTags.spec.ts │ │ │ └── HtmlTags.tsx │ │ ├── Icon │ │ │ ├── AdmonitionDanger.tsx │ │ │ ├── AdmonitionInfo.tsx │ │ │ ├── AdmonitionNote.tsx │ │ │ ├── AdmonitionTip.tsx │ │ │ ├── AdmonitionWarning.tsx │ │ │ ├── ApiIcon.tsx │ │ │ ├── ArrowDropDown.module.scss │ │ │ ├── ArrowDropDown.tsx │ │ │ ├── ArrowDropUp.module.scss │ │ │ ├── ArrowDropUp.tsx │ │ │ ├── ArrowLeft.module.scss │ │ │ ├── ArrowLeft.tsx │ │ │ ├── ArrowRight.module.scss │ │ │ ├── ArrowRight.tsx │ │ │ ├── Attach.tsx │ │ │ ├── Binding.module.scss │ │ │ ├── Binding.tsx │ │ │ ├── BoardIcon.tsx │ │ │ ├── BoxIcon.tsx │ │ │ ├── CheckIcon.tsx │ │ │ ├── ChevronDownIcon.tsx │ │ │ ├── ChevronLeft.tsx │ │ │ ├── ChevronRight.tsx │ │ │ ├── ChevronUpIcon.tsx │ │ │ ├── CodeFileIcon.tsx │ │ │ ├── CodeSandbox.tsx │ │ │ ├── CompactListIcon.tsx │ │ │ ├── ContentCopyIcon.tsx │ │ │ ├── DarkToLightIcon.tsx │ │ │ ├── DatabaseIcon.module.scss │ │ │ ├── DatabaseIcon.tsx │ │ │ ├── DocFileIcon.tsx │ │ │ ├── DocIcon.tsx │ │ │ ├── DotMenuHorizontalIcon.tsx │ │ │ ├── DotMenuIcon.tsx │ │ │ ├── EmailIcon.tsx │ │ │ ├── EmptyFolderIcon.tsx │ │ │ ├── ErrorIcon.tsx │ │ │ ├── ExpressionIcon.tsx │ │ │ ├── FillPlusCricleIcon.tsx │ │ │ ├── FilterIcon.tsx │ │ │ ├── FolderIcon.tsx │ │ │ ├── GlobeIcon.tsx │ │ │ ├── HomeIcon.tsx │ │ │ ├── HyperLinkIcon.tsx │ │ │ ├── Icon.md │ │ │ ├── Icon.module.scss │ │ │ ├── Icon.spec.ts │ │ │ ├── Icon.tsx │ │ │ ├── IconNative.tsx │ │ │ ├── ImageFileIcon.tsx │ │ │ ├── Inspect.tsx │ │ │ ├── LightToDark.tsx │ │ │ ├── LinkIcon.tsx │ │ │ ├── ListIcon.tsx │ │ │ ├── LooseListIcon.tsx │ │ │ ├── MoonIcon.tsx │ │ │ ├── MoreOptionsIcon.tsx │ │ │ ├── NoSortIcon.tsx │ │ │ ├── PDFIcon.tsx │ │ │ ├── PenIcon.tsx │ │ │ ├── PhoneIcon.tsx │ │ │ ├── PhotoIcon.tsx │ │ │ ├── PlusIcon.tsx │ │ │ ├── SearchIcon.tsx │ │ │ ├── ShareIcon.tsx │ │ │ ├── SortAscendingIcon.tsx │ │ │ ├── SortDescendingIcon.tsx │ │ │ ├── StarsIcon.tsx │ │ │ ├── SunIcon.tsx │ │ │ ├── svg │ │ │ │ ├── admonition_danger.svg │ │ │ │ ├── admonition_info.svg │ │ │ │ ├── admonition_note.svg │ │ │ │ ├── admonition_tip.svg │ │ │ │ ├── admonition_warning.svg │ │ │ │ ├── api.svg │ │ │ │ ├── arrow-dropdown.svg │ │ │ │ ├── arrow-left.svg │ │ │ │ ├── arrow-right.svg │ │ │ │ ├── arrow-up.svg │ │ │ │ ├── attach.svg │ │ │ │ ├── binding.svg │ │ │ │ ├── box.svg │ │ │ │ ├── bulb.svg │ │ │ │ ├── code-file.svg │ │ │ │ ├── code-sandbox.svg │ │ │ │ ├── dark_to_light.svg │ │ │ │ ├── database.svg │ │ │ │ ├── doc.svg │ │ │ │ ├── empty-folder.svg │ │ │ │ ├── expression.svg │ │ │ │ ├── eye-closed.svg │ │ │ │ ├── eye-dark.svg │ │ │ │ ├── eye.svg │ │ │ │ ├── file-text.svg │ │ │ │ ├── filter.svg │ │ │ │ ├── folder.svg │ │ │ │ ├── img.svg │ │ │ │ ├── inspect.svg │ │ │ │ ├── light_to_dark.svg │ │ │ │ ├── moon.svg │ │ │ │ ├── pdf.svg │ │ │ │ ├── photo.svg │ │ │ │ ├── share.svg │ │ │ │ ├── stars.svg │ │ │ │ ├── sun.svg │ │ │ │ ├── trending-down.svg │ │ │ │ ├── trending-level.svg │ │ │ │ ├── trending-up.svg │ │ │ │ ├── txt.svg │ │ │ │ ├── unknown-file.svg │ │ │ │ ├── unlink.svg │ │ │ │ └── xls.svg │ │ │ ├── TableDeleteColumnIcon.tsx │ │ │ ├── TableDeleteRowIcon.tsx │ │ │ ├── TableInsertColumnIcon.tsx │ │ │ ├── TableInsertRowIcon.tsx │ │ │ ├── TrashIcon.tsx │ │ │ ├── TrendingDownIcon.tsx │ │ │ ├── TrendingLevelIcon.tsx │ │ │ ├── TrendingUpIcon.tsx │ │ │ ├── TxtIcon.tsx │ │ │ ├── UnknownFileIcon.tsx │ │ │ ├── UnlinkIcon.tsx │ │ │ ├── UserIcon.tsx │ │ │ ├── WarningIcon.tsx │ │ │ └── XlsIcon.tsx │ │ ├── IconProvider.tsx │ │ ├── IconRegistryContext.tsx │ │ ├── IFrame │ │ │ ├── IFrame.md │ │ │ ├── IFrame.module.scss │ │ │ ├── IFrame.spec.ts │ │ │ ├── IFrame.tsx │ │ │ └── IFrameNative.tsx │ │ ├── Image │ │ │ ├── Image.md │ │ │ ├── Image.module.scss │ │ │ ├── Image.spec.ts │ │ │ ├── Image.tsx │ │ │ └── ImageNative.tsx │ │ ├── Input │ │ │ ├── index.ts │ │ │ ├── InputAdornment.module.scss │ │ │ ├── InputAdornment.tsx │ │ │ ├── InputDivider.module.scss │ │ │ ├── InputDivider.tsx │ │ │ ├── InputLabel.module.scss │ │ │ ├── InputLabel.tsx │ │ │ ├── PartialInput.module.scss │ │ │ └── PartialInput.tsx │ │ ├── InspectButton │ │ │ ├── InspectButton.module.scss │ │ │ └── InspectButton.tsx │ │ ├── Items │ │ │ ├── Items.md │ │ │ ├── Items.spec.ts │ │ │ ├── Items.tsx │ │ │ └── ItemsNative.tsx │ │ ├── Link │ │ │ ├── Link.md │ │ │ ├── Link.module.scss │ │ │ ├── Link.spec.ts │ │ │ ├── Link.tsx │ │ │ └── LinkNative.tsx │ │ ├── List │ │ │ ├── doc-resources │ │ │ │ └── list-component-data.js │ │ │ ├── List.md │ │ │ ├── List.module.scss │ │ │ ├── List.spec.ts │ │ │ ├── List.tsx │ │ │ └── ListNative.tsx │ │ ├── Logo │ │ │ ├── doc-resources │ │ │ │ └── xmlui-logo.svg │ │ │ ├── Logo.md │ │ │ ├── Logo.tsx │ │ │ └── LogoNative.tsx │ │ ├── Markdown │ │ │ ├── CodeText.module.scss │ │ │ ├── CodeText.tsx │ │ │ ├── Markdown.md │ │ │ ├── Markdown.module.scss │ │ │ ├── Markdown.spec.ts │ │ │ ├── Markdown.tsx │ │ │ ├── MarkdownNative.tsx │ │ │ ├── parse-binding-expr.ts │ │ │ └── utils.ts │ │ ├── metadata-helpers.ts │ │ ├── ModalDialog │ │ │ ├── ConfirmationModalContextProvider.tsx │ │ │ ├── Dialog.module.scss │ │ │ ├── Dialog.tsx │ │ │ ├── ModalDialog.md │ │ │ ├── ModalDialog.module.scss │ │ │ ├── ModalDialog.spec.ts │ │ │ ├── ModalDialog.tsx │ │ │ ├── ModalDialogNative.tsx │ │ │ └── ModalVisibilityContext.tsx │ │ ├── NavGroup │ │ │ ├── NavGroup.md │ │ │ ├── NavGroup.module.scss │ │ │ ├── NavGroup.spec.ts │ │ │ ├── NavGroup.tsx │ │ │ ├── NavGroupContext.ts │ │ │ └── NavGroupNative.tsx │ │ ├── NavLink │ │ │ ├── NavLink.md │ │ │ ├── NavLink.module.scss │ │ │ ├── NavLink.spec.ts │ │ │ ├── NavLink.tsx │ │ │ └── NavLinkNative.tsx │ │ ├── NavPanel │ │ │ ├── NavPanel.md │ │ │ ├── NavPanel.module.scss │ │ │ ├── NavPanel.spec.ts │ │ │ ├── NavPanel.tsx │ │ │ └── NavPanelNative.tsx │ │ ├── NestedApp │ │ │ ├── AppWithCodeView.module.scss │ │ │ ├── AppWithCodeView.tsx │ │ │ ├── AppWithCodeViewNative.tsx │ │ │ ├── defaultProps.tsx │ │ │ ├── logo.svg │ │ │ ├── NestedApp.module.scss │ │ │ ├── NestedApp.tsx │ │ │ ├── NestedAppNative.tsx │ │ │ ├── Tooltip.module.scss │ │ │ ├── Tooltip.tsx │ │ │ └── utils.ts │ │ ├── NoResult │ │ │ ├── NoResult.md │ │ │ ├── NoResult.module.scss │ │ │ ├── NoResult.spec.ts │ │ │ ├── NoResult.tsx │ │ │ └── NoResultNative.tsx │ │ ├── NumberBox │ │ │ ├── numberbox-abstractions.ts │ │ │ ├── NumberBox.md │ │ │ ├── NumberBox.module.scss │ │ │ ├── NumberBox.spec.ts │ │ │ ├── NumberBox.tsx │ │ │ └── NumberBoxNative.tsx │ │ ├── Option │ │ │ ├── Option.md │ │ │ ├── Option.spec.ts │ │ │ ├── Option.tsx │ │ │ ├── OptionNative.tsx │ │ │ └── OptionTypeProvider.tsx │ │ ├── PageMetaTitle │ │ │ ├── PageMetaTilteNative.tsx │ │ │ ├── PageMetaTitle.md │ │ │ ├── PageMetaTitle.spec.ts │ │ │ └── PageMetaTitle.tsx │ │ ├── Pages │ │ │ ├── Page.md │ │ │ ├── Pages.md │ │ │ ├── Pages.module.scss │ │ │ ├── Pages.tsx │ │ │ └── PagesNative.tsx │ │ ├── Pagination │ │ │ ├── Pagination.md │ │ │ ├── Pagination.module.scss │ │ │ ├── Pagination.spec.ts │ │ │ ├── Pagination.tsx │ │ │ └── PaginationNative.tsx │ │ ├── PositionedContainer │ │ │ ├── PositionedContainer.module.scss │ │ │ ├── PositionedContainer.tsx │ │ │ └── PositionedContainerNative.tsx │ │ ├── ProfileMenu │ │ │ ├── ProfileMenu.module.scss │ │ │ └── ProfileMenu.tsx │ │ ├── ProgressBar │ │ │ ├── ProgressBar.md │ │ │ ├── ProgressBar.module.scss │ │ │ ├── ProgressBar.spec.ts │ │ │ ├── ProgressBar.tsx │ │ │ └── ProgressBarNative.tsx │ │ ├── Queue │ │ │ ├── Queue.md │ │ │ ├── Queue.spec.ts │ │ │ ├── Queue.tsx │ │ │ ├── queueActions.ts │ │ │ └── QueueNative.tsx │ │ ├── RadioGroup │ │ │ ├── RadioGroup.md │ │ │ ├── RadioGroup.module.scss │ │ │ ├── RadioGroup.spec.ts │ │ │ ├── RadioGroup.tsx │ │ │ ├── RadioGroupNative.tsx │ │ │ ├── RadioItem.tsx │ │ │ └── RadioItemNative.tsx │ │ ├── RealTimeAdapter │ │ │ ├── RealTimeAdapter.tsx │ │ │ └── RealTimeAdapterNative.tsx │ │ ├── Redirect │ │ │ ├── Redirect.md │ │ │ ├── Redirect.spec.ts │ │ │ └── Redirect.tsx │ │ ├── ResponsiveBar │ │ │ ├── README.md │ │ │ ├── ResponsiveBar.md │ │ │ ├── ResponsiveBar.module.scss │ │ │ ├── ResponsiveBar.spec.ts │ │ │ ├── ResponsiveBar.tsx │ │ │ └── ResponsiveBarNative.tsx │ │ ├── Select │ │ │ ├── HiddenOption.tsx │ │ │ ├── MultiSelectOption.tsx │ │ │ ├── OptionContext.ts │ │ │ ├── Select.md │ │ │ ├── Select.module.scss │ │ │ ├── Select.spec.ts │ │ │ ├── Select.tsx │ │ │ ├── SelectContext.tsx │ │ │ ├── SelectNative.tsx │ │ │ ├── SelectOption.tsx │ │ │ └── SimpleSelect.tsx │ │ ├── SelectionStore │ │ │ ├── SelectionStore.md │ │ │ ├── SelectionStore.tsx │ │ │ └── SelectionStoreNative.tsx │ │ ├── Slider │ │ │ ├── Slider.md │ │ │ ├── Slider.module.scss │ │ │ ├── Slider.spec.ts │ │ │ ├── Slider.tsx │ │ │ └── SliderNative.tsx │ │ ├── Slot │ │ │ ├── Slot.md │ │ │ ├── Slot.spec.ts │ │ │ └── Slot.ts │ │ ├── SlotItem.tsx │ │ ├── SpaceFiller │ │ │ ├── SpaceFiller.md │ │ │ ├── SpaceFiller.module.scss │ │ │ ├── SpaceFiller.spec.ts │ │ │ ├── SpaceFiller.tsx │ │ │ └── SpaceFillerNative.tsx │ │ ├── Spinner │ │ │ ├── Spinner.md │ │ │ ├── Spinner.module.scss │ │ │ ├── Spinner.spec.ts │ │ │ ├── Spinner.tsx │ │ │ └── SpinnerNative.tsx │ │ ├── Splitter │ │ │ ├── HSplitter.md │ │ │ ├── HSplitter.spec.ts │ │ │ ├── Splitter.md │ │ │ ├── Splitter.module.scss │ │ │ ├── Splitter.spec.ts │ │ │ ├── Splitter.tsx │ │ │ ├── SplitterNative.tsx │ │ │ ├── utils.ts │ │ │ ├── VSplitter.md │ │ │ └── VSplitter.spec.ts │ │ ├── Stack │ │ │ ├── CHStack.md │ │ │ ├── CHStack.spec.ts │ │ │ ├── CVStack.md │ │ │ ├── CVStack.spec.ts │ │ │ ├── HStack.md │ │ │ ├── HStack.spec.ts │ │ │ ├── Stack.md │ │ │ ├── Stack.module.scss │ │ │ ├── Stack.spec.ts │ │ │ ├── Stack.tsx │ │ │ ├── StackNative.tsx │ │ │ ├── VStack.md │ │ │ └── VStack.spec.ts │ │ ├── StickyBox │ │ │ ├── StickyBox.md │ │ │ ├── StickyBox.module.scss │ │ │ ├── StickyBox.tsx │ │ │ └── StickyBoxNative.tsx │ │ ├── Switch │ │ │ ├── Switch.md │ │ │ ├── Switch.spec.ts │ │ │ └── Switch.tsx │ │ ├── Table │ │ │ ├── doc-resources │ │ │ │ └── list-component-data.js │ │ │ ├── react-table-config.d.ts │ │ │ ├── Table.md │ │ │ ├── Table.module.scss │ │ │ ├── Table.spec.ts │ │ │ ├── Table.tsx │ │ │ ├── TableNative.tsx │ │ │ └── useRowSelection.tsx │ │ ├── TableOfContents │ │ │ ├── TableOfContents.module.scss │ │ │ ├── TableOfContents.spec.ts │ │ │ ├── TableOfContents.tsx │ │ │ └── TableOfContentsNative.tsx │ │ ├── Tabs │ │ │ ├── TabContext.tsx │ │ │ ├── TabItem.md │ │ │ ├── TabItem.tsx │ │ │ ├── TabItemNative.tsx │ │ │ ├── Tabs.md │ │ │ ├── Tabs.module.scss │ │ │ ├── Tabs.spec.ts │ │ │ ├── Tabs.tsx │ │ │ └── TabsNative.tsx │ │ ├── Text │ │ │ ├── Text.md │ │ │ ├── Text.module.scss │ │ │ ├── Text.spec.ts │ │ │ ├── Text.tsx │ │ │ └── TextNative.tsx │ │ ├── TextArea │ │ │ ├── TextArea.md │ │ │ ├── TextArea.module.scss │ │ │ ├── TextArea.spec.ts │ │ │ ├── TextArea.tsx │ │ │ ├── TextAreaNative.tsx │ │ │ ├── TextAreaResizable.tsx │ │ │ └── useComposedRef.ts │ │ ├── TextBox │ │ │ ├── TextBox.md │ │ │ ├── TextBox.module.scss │ │ │ ├── TextBox.spec.ts │ │ │ ├── TextBox.tsx │ │ │ └── TextBoxNative.tsx │ │ ├── Theme │ │ │ ├── NotificationToast.tsx │ │ │ ├── Theme.md │ │ │ ├── Theme.module.scss │ │ │ ├── Theme.spec.ts │ │ │ ├── Theme.tsx │ │ │ └── ThemeNative.tsx │ │ ├── TimeInput │ │ │ ├── TimeInput.md │ │ │ ├── TimeInput.module.scss │ │ │ ├── TimeInput.spec.ts │ │ │ ├── TimeInput.tsx │ │ │ ├── TimeInputNative.tsx │ │ │ └── utils.ts │ │ ├── Timer │ │ │ ├── Timer.md │ │ │ ├── Timer.spec.ts │ │ │ ├── Timer.tsx │ │ │ └── TimerNative.tsx │ │ ├── Toggle │ │ │ ├── Toggle.module.scss │ │ │ └── Toggle.tsx │ │ ├── ToneChangerButton │ │ │ ├── ToneChangerButton.md │ │ │ ├── ToneChangerButton.spec.ts │ │ │ └── ToneChangerButton.tsx │ │ ├── ToneSwitch │ │ │ ├── ToneSwitch.md │ │ │ ├── ToneSwitch.module.scss │ │ │ ├── ToneSwitch.spec.ts │ │ │ ├── ToneSwitch.tsx │ │ │ └── ToneSwitchNative.tsx │ │ ├── Tooltip │ │ │ ├── Tooltip.md │ │ │ ├── Tooltip.module.scss │ │ │ ├── Tooltip.spec.ts │ │ │ ├── Tooltip.tsx │ │ │ └── TooltipNative.tsx │ │ ├── Tree │ │ │ ├── testData.ts │ │ │ ├── Tree-dynamic.spec.ts │ │ │ ├── Tree-icons.spec.ts │ │ │ ├── Tree.md │ │ │ ├── Tree.spec.ts │ │ │ ├── TreeComponent.module.scss │ │ │ ├── TreeComponent.tsx │ │ │ └── TreeNative.tsx │ │ ├── TreeDisplay │ │ │ ├── TreeDisplay.md │ │ │ ├── TreeDisplay.module.scss │ │ │ ├── TreeDisplay.tsx │ │ │ └── TreeDisplayNative.tsx │ │ ├── ValidationSummary │ │ │ ├── ValidationSummary.module.scss │ │ │ └── ValidationSummary.tsx │ │ └── VisuallyHidden.tsx │ ├── components-core │ │ ├── abstractions │ │ │ ├── ComponentRenderer.ts │ │ │ ├── LoaderRenderer.ts │ │ │ ├── standalone.ts │ │ │ └── treeAbstractions.ts │ │ ├── action │ │ │ ├── actions.ts │ │ │ ├── APICall.tsx │ │ │ ├── FileDownloadAction.tsx │ │ │ ├── FileUploadAction.tsx │ │ │ ├── NavigateAction.tsx │ │ │ └── TimedAction.tsx │ │ ├── ApiBoundComponent.tsx │ │ ├── appContext │ │ │ ├── date-functions.ts │ │ │ ├── math-function.ts │ │ │ └── misc-utils.ts │ │ ├── AppContext.tsx │ │ ├── behaviors │ │ │ ├── Behavior.tsx │ │ │ └── CoreBehaviors.tsx │ │ ├── component-hooks.ts │ │ ├── ComponentDecorator.tsx │ │ ├── ComponentViewer.tsx │ │ ├── CompoundComponent.tsx │ │ ├── constants.ts │ │ ├── DebugViewProvider.tsx │ │ ├── descriptorHelper.ts │ │ ├── devtools │ │ │ ├── InspectorDialog.module.scss │ │ │ ├── InspectorDialog.tsx │ │ │ └── InspectorDialogVisibilityContext.tsx │ │ ├── EngineError.ts │ │ ├── event-handlers.ts │ │ ├── InspectorButton.module.scss │ │ ├── InspectorContext.tsx │ │ ├── interception │ │ │ ├── abstractions.ts │ │ │ ├── ApiInterceptor.ts │ │ │ ├── ApiInterceptorProvider.tsx │ │ │ ├── apiInterceptorWorker.ts │ │ │ ├── Backend.ts │ │ │ ├── Errors.ts │ │ │ ├── IndexedDb.ts │ │ │ ├── initMock.ts │ │ │ ├── InMemoryDb.ts │ │ │ ├── ReadonlyCollection.ts │ │ │ └── useApiInterceptorContext.tsx │ │ ├── loader │ │ │ ├── ApiLoader.tsx │ │ │ ├── DataLoader.tsx │ │ │ ├── ExternalDataLoader.tsx │ │ │ ├── Loader.tsx │ │ │ ├── MockLoaderRenderer.tsx │ │ │ └── PageableLoader.tsx │ │ ├── LoaderComponent.tsx │ │ ├── markup-check.ts │ │ ├── parts.ts │ │ ├── renderers.ts │ │ ├── rendering │ │ │ ├── AppContent.tsx │ │ │ ├── AppRoot.tsx │ │ │ ├── AppWrapper.tsx │ │ │ ├── buildProxy.ts │ │ │ ├── collectFnVarDeps.ts │ │ │ ├── ComponentAdapter.tsx │ │ │ ├── ComponentWrapper.tsx │ │ │ ├── Container.tsx │ │ │ ├── containers.ts │ │ │ ├── ContainerWrapper.tsx │ │ │ ├── ErrorBoundary.module.scss │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── InvalidComponent.module.scss │ │ │ ├── InvalidComponent.tsx │ │ │ ├── nodeUtils.ts │ │ │ ├── reducer.ts │ │ │ ├── renderChild.tsx │ │ │ ├── StandaloneComponent.tsx │ │ │ ├── StateContainer.tsx │ │ │ ├── UnknownComponent.module.scss │ │ │ ├── UnknownComponent.tsx │ │ │ └── valueExtractor.ts │ │ ├── reportEngineError.ts │ │ ├── RestApiProxy.ts │ │ ├── script-runner │ │ │ ├── asyncProxy.ts │ │ │ ├── AttributeValueParser.ts │ │ │ ├── bannedFunctions.ts │ │ │ ├── BindingTreeEvaluationContext.ts │ │ │ ├── eval-tree-async.ts │ │ │ ├── eval-tree-common.ts │ │ │ ├── eval-tree-sync.ts │ │ │ ├── ParameterParser.ts │ │ │ ├── process-statement-async.ts │ │ │ ├── process-statement-common.ts │ │ │ ├── process-statement-sync.ts │ │ │ ├── ScriptingSourceTree.ts │ │ │ ├── simplify-expression.ts │ │ │ ├── statement-queue.ts │ │ │ └── visitors.ts │ │ ├── StandaloneApp.tsx │ │ ├── StandaloneExtensionManager.ts │ │ ├── TableOfContentsContext.tsx │ │ ├── theming │ │ │ ├── _themes.scss │ │ │ ├── component-layout-resolver.ts │ │ │ ├── extendThemeUtils.ts │ │ │ ├── hvar.ts │ │ │ ├── layout-resolver.ts │ │ │ ├── parse-layout-props.ts │ │ │ ├── StyleContext.tsx │ │ │ ├── StyleRegistry.ts │ │ │ ├── ThemeContext.tsx │ │ │ ├── ThemeProvider.tsx │ │ │ ├── themes │ │ │ │ ├── base-utils.ts │ │ │ │ ├── palette.ts │ │ │ │ ├── root.ts │ │ │ │ ├── solid.ts │ │ │ │ ├── theme-colors.ts │ │ │ │ └── xmlui.ts │ │ │ ├── themeVars.module.scss │ │ │ ├── themeVars.ts │ │ │ ├── transformThemeVars.ts │ │ │ └── utils.ts │ │ ├── utils │ │ │ ├── actionUtils.ts │ │ │ ├── audio-utils.ts │ │ │ ├── compound-utils.ts │ │ │ ├── css-utils.ts │ │ │ ├── DataLoaderQueryKeyGenerator.ts │ │ │ ├── date-utils.ts │ │ │ ├── extractParam.ts │ │ │ ├── hooks.tsx │ │ │ ├── LruCache.ts │ │ │ ├── mergeProps.ts │ │ │ ├── misc.ts │ │ │ ├── request-params.ts │ │ │ ├── statementUtils.ts │ │ │ └── treeUtils.ts │ │ └── xmlui-parser.ts │ ├── index-standalone.ts │ ├── index.scss │ ├── index.ts │ ├── language-server │ │ ├── server-common.ts │ │ ├── server-web-worker.ts │ │ ├── server.ts │ │ ├── services │ │ │ ├── common │ │ │ │ ├── docs-generation.ts │ │ │ │ ├── lsp-utils.ts │ │ │ │ ├── metadata-utils.ts │ │ │ │ └── syntax-node-utilities.ts │ │ │ ├── completion.ts │ │ │ ├── diagnostic.ts │ │ │ ├── format.ts │ │ │ └── hover.ts │ │ └── xmlui-metadata-generated.mjs │ ├── logging │ │ ├── LoggerContext.tsx │ │ ├── LoggerInitializer.tsx │ │ ├── LoggerService.ts │ │ └── xmlui.ts │ ├── logo.svg │ ├── parsers │ │ ├── common │ │ │ ├── GenericToken.ts │ │ │ ├── InputStream.ts │ │ │ └── utils.ts │ │ ├── scripting │ │ │ ├── code-behind-collect.ts │ │ │ ├── Lexer.ts │ │ │ ├── modules.ts │ │ │ ├── Parser.ts │ │ │ ├── ParserError.ts │ │ │ ├── ScriptingNodeTypes.ts │ │ │ ├── TokenTrait.ts │ │ │ ├── TokenType.ts │ │ │ └── tree-visitor.ts │ │ ├── style-parser │ │ │ ├── errors.ts │ │ │ ├── source-tree.ts │ │ │ ├── StyleInputStream.ts │ │ │ ├── StyleLexer.ts │ │ │ ├── StyleParser.ts │ │ │ └── tokens.ts │ │ └── xmlui-parser │ │ ├── CharacterCodes.ts │ │ ├── diagnostics.ts │ │ ├── fileExtensions.ts │ │ ├── index.ts │ │ ├── lint.ts │ │ ├── parser.ts │ │ ├── ParserError.ts │ │ ├── scanner.ts │ │ ├── syntax-kind.ts │ │ ├── syntax-node.ts │ │ ├── transform.ts │ │ ├── utils.ts │ │ ├── xmlui-serializer.ts │ │ └── xmlui-tree.ts │ ├── react-app-env.d.ts │ ├── syntax │ │ ├── monaco │ │ │ ├── grammar.monacoLanguage.ts │ │ │ ├── index.ts │ │ │ ├── xmlui-dark.ts │ │ │ ├── xmlui-light.ts │ │ │ └── xmluiscript.monacoLanguage.ts │ │ └── textMate │ │ ├── index.ts │ │ ├── xmlui-dark.json │ │ ├── xmlui-light.json │ │ ├── xmlui.json │ │ └── xmlui.tmLanguage.json │ ├── testing │ │ ├── assertions.ts │ │ ├── component-test-helpers.ts │ │ ├── ComponentDrivers.ts │ │ ├── drivers │ │ │ ├── DateInputDriver.ts │ │ │ ├── ModalDialogDriver.ts │ │ │ ├── NumberBoxDriver.ts │ │ │ ├── TextBoxDriver.ts │ │ │ ├── TimeInputDriver.ts │ │ │ ├── TimerDriver.ts │ │ │ └── TreeDriver.ts │ │ ├── fixtures.ts │ │ ├── infrastructure │ │ │ ├── index.html │ │ │ ├── main.tsx │ │ │ ├── public │ │ │ │ ├── mockServiceWorker.js │ │ │ │ ├── resources │ │ │ │ │ ├── bell.svg │ │ │ │ │ ├── box.svg │ │ │ │ │ ├── doc.svg │ │ │ │ │ ├── eye.svg │ │ │ │ │ ├── flower-640x480.jpg │ │ │ │ │ ├── sun.svg │ │ │ │ │ ├── test-image-100x100.jpg │ │ │ │ │ └── txt.svg │ │ │ │ └── serve.json │ │ │ └── TestBed.tsx │ │ └── themed-app-test-helpers.ts │ └── vite-env.d.ts ├── tests │ ├── components │ │ ├── CodeBlock │ │ │ └── hightlight-code.test.ts │ │ ├── playground-pattern.test.ts │ │ └── Tree │ │ └── Tree-states.test.ts │ ├── components-core │ │ ├── abstractions │ │ │ └── treeAbstractions.test.ts │ │ ├── container │ │ │ └── buildProxy.test.ts │ │ ├── interception │ │ │ ├── orderBy.test.ts │ │ │ ├── ReadOnlyCollection.test.ts │ │ │ └── request-param-converter.test.ts │ │ ├── scripts-runner │ │ │ ├── AttributeValueParser.test.ts │ │ │ ├── eval-tree-arrow-async.test.ts │ │ │ ├── eval-tree-arrow.test.ts │ │ │ ├── eval-tree-func-decl-async.test.ts │ │ │ ├── eval-tree-func-decl.test.ts │ │ │ ├── eval-tree-pre-post.test.ts │ │ │ ├── eval-tree-regression.test.ts │ │ │ ├── eval-tree.test.ts │ │ │ ├── function-proxy.test.ts │ │ │ ├── parser-regression.test.ts │ │ │ ├── process-event.test.ts │ │ │ ├── process-function.test.ts │ │ │ ├── process-implicit-context.test.ts │ │ │ ├── process-statement-asgn.test.ts │ │ │ ├── process-statement-destruct.test.ts │ │ │ ├── process-statement-regs.test.ts │ │ │ ├── process-statement-sync.test.ts │ │ │ ├── process-statement.test.ts │ │ │ ├── process-switch-sync.test.ts │ │ │ ├── process-switch.test.ts │ │ │ ├── process-try-sync.test.ts │ │ │ ├── process-try.test.ts │ │ │ └── test-helpers.ts │ │ ├── test-metadata-handler.ts │ │ ├── theming │ │ │ ├── border-segments.test.ts │ │ │ ├── component-layout.resolver.test.ts │ │ │ ├── layout-property-parser.test.ts │ │ │ ├── layout-resolver.test.ts │ │ │ ├── layout-resolver2.test.ts │ │ │ ├── layout-vp-override.test.ts │ │ │ └── padding-segments.test.ts │ │ └── utils │ │ ├── date-utils.test.ts │ │ ├── format-human-elapsed-time.test.ts │ │ └── LruCache.test.ts │ ├── language-server │ │ ├── completion.test.ts │ │ ├── format.test.ts │ │ ├── hover.test.ts │ │ └── mockData.ts │ └── parsers │ ├── common │ │ └── input-stream.test.ts │ ├── markdown │ │ └── parse-binding-expression.test.ts │ ├── parameter-parser.test.ts │ ├── paremeter-parser.test.ts │ ├── scripting │ │ ├── eval-tree-arrow.test.ts │ │ ├── eval-tree-pre-post.test.ts │ │ ├── eval-tree.test.ts │ │ ├── function-proxy.test.ts │ │ ├── lexer-literals.test.ts │ │ ├── lexer-misc.test.ts │ │ ├── module-parse.test.ts │ │ ├── parser-arrow.test.ts │ │ ├── parser-assignments.test.ts │ │ ├── parser-binary.test.ts │ │ ├── parser-destructuring.test.ts │ │ ├── parser-errors.test.ts │ │ ├── parser-expressions.test.ts │ │ ├── parser-function.test.ts │ │ ├── parser-literals.test.ts │ │ ├── parser-primary.test.ts │ │ ├── parser-regex.test.ts │ │ ├── parser-statements.test.ts │ │ ├── parser-unary.test.ts │ │ ├── process-event.test.ts │ │ ├── process-implicit-context.test.ts │ │ ├── process-statement-asgn.test.ts │ │ ├── process-statement-destruct.test.ts │ │ ├── process-statement-regs.test.ts │ │ ├── process-statement-sync.test.ts │ │ ├── process-statement.test.ts │ │ ├── process-switch-sync.test.ts │ │ ├── process-switch.test.ts │ │ ├── process-try-sync.test.ts │ │ ├── process-try.test.ts │ │ ├── simplify-expression.test.ts │ │ ├── statement-hooks.test.ts │ │ └── test-helpers.ts │ ├── style-parser │ │ ├── generateHvarChain.test.ts │ │ ├── parseHVar.test.ts │ │ ├── parser.test.ts │ │ └── tokens.test.ts │ └── xmlui │ ├── lint.test.ts │ ├── parser.test.ts │ ├── scanner.test.ts │ ├── transform.attr.test.ts │ ├── transform.circular.test.ts │ ├── transform.element.test.ts │ ├── transform.errors.test.ts │ ├── transform.escape.test.ts │ ├── transform.regression.test.ts │ ├── transform.script.test.ts │ ├── transform.test.ts │ └── xmlui.ts ├── tests-e2e │ ├── api-bound-component-regression.spec.ts │ ├── api-call-as-extracted-component.spec.ts │ ├── assign-to-object-or-array-regression.spec.ts │ ├── binding-regression.spec.ts │ ├── children-as-template-context-vars.spec.ts │ ├── compound-component.spec.ts │ ├── context-vars-regression.spec.ts │ ├── data-bindings.spec.ts │ ├── datasource-and-api-usage-in-var.spec.ts │ ├── datasource-direct-binding.spec.ts │ ├── datasource-onLoaded-regression.spec.ts │ ├── modify-array-item-regression.spec.ts │ ├── namespaces.spec.ts │ ├── push-to-array-regression.spec.ts │ ├── screen-breakpoints.spec.ts │ ├── scripting.spec.ts │ ├── state-scope-in-pages.spec.ts │ └── state-var-scopes.spec.ts ├── tsconfig.bin.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── vitest.config.ts ``` # Files -------------------------------------------------------------------------------- /xmlui/dev-docs/react-fundamentals.md: -------------------------------------------------------------------------------- ```markdown 1 | # React Fundamentals 2 | 3 | This document is a concise reference for React patterns and hooks used in XMLUI. It assumes you're familiar with [React basics](https://react.dev/learn) and provides practical guidance for reading and maintaining XMLUI source code. 4 | 5 | ## React Hooks: Quick Overview 6 | 7 | **What are hooks?** Functions that let you "hook into" React features from function components. They always start with `use` (e.g., `useState`, `useEffect`). 8 | 9 | **Core hooks in XMLUI:** 10 | - **`useState`** - Add local state to components 11 | - **`useEffect`** - Run side effects (data fetching, subscriptions, DOM updates) 12 | - **`useRef`** - Store mutable values that don't trigger re-renders 13 | - **`useMemo`** - Cache expensive calculations 14 | - **`useCallback`** - Cache function definitions 15 | - **`useReducer`** - Manage complex state with reducer pattern 16 | - **`useContext`** - Access context values from providers 17 | 18 | **Two critical rules:** 19 | 1. Only call hooks at the top level (not in loops, conditions, or nested functions) 20 | 2. Only call hooks from React functions (components or custom hooks) 21 | 22 | **Why?** React tracks hooks by call order. Breaking these rules causes state mismatches and bugs. 23 | 24 | ## Component Rendering Lifecycle 25 | 26 | React components re-render whenever their state or props change. Understanding this cycle prevents performance issues and unexpected behavior. 27 | 28 | **The 5-Phase Render Cycle:** 29 | 30 | 1. **Trigger** → Something requests a render: 31 | - Parent component re-renders 32 | - Props change 33 | - State changes (`useState`, `useReducer`) 34 | 35 | 2. **Render** → React calls your component function, which returns JSX 36 | 37 | 3. **Reconciliation** → React's diffing algorithm determines what changed in the virtual DOM 38 | 39 | 4. **Commit** → React updates the actual DOM with minimal changes 40 | 41 | 5. **Effects** → React runs effects in order: 42 | - `useLayoutEffect` (synchronous, blocks paint) 43 | - Browser paints the screen 44 | - `useEffect` (asynchronous, after paint) 45 | 46 | **Key insight:** Re-rendering is cheap (just a function call), but DOM updates are expensive. React optimizes by batching and minimizing DOM changes. 47 | 48 | **Common performance pitfalls:** 49 | ```tsx 50 | // ❌ WRONG - Parent re-renders cause all children to re-render 51 | function Parent() { 52 | const [count, setCount] = useState(0); 53 | return ( 54 | <div> 55 | <ExpensiveChild data={data} /> {/* Re-renders unnecessarily */} 56 | <button onClick={() => setCount(c => c + 1)}>Update</button> 57 | </div> 58 | ); 59 | } 60 | 61 | // ✅ CORRECT - Memoize to prevent unnecessary re-renders 62 | const MemoizedChild = React.memo(ExpensiveChild); 63 | 64 | function Parent() { 65 | const [count, setCount] = useState(0); 66 | const data = useMemo(() => computeData(), []); // Stable reference 67 | return ( 68 | <div> 69 | <MemoizedChild data={data} /> {/* Only re-renders when data changes */} 70 | <button onClick={() => setCount(c => c + 1)}>Update</button> 71 | </div> 72 | ); 73 | } 74 | ``` 75 | 76 | ## Rules of Hooks 77 | 78 | These rules are enforced by React and ESLint. Violating them causes hard-to-debug errors. 79 | 80 | **Rule 1: Call hooks at the top level** 81 | ```tsx 82 | // ❌ WRONG - Conditional hook 83 | function Bad({ show }: Props) { 84 | if (show) { 85 | const [value, setValue] = useState(""); // Hook order changes! 86 | } 87 | return <div>...</div>; 88 | } 89 | 90 | // ✅ CORRECT - Hook always called 91 | function Good({ show }: Props) { 92 | const [value, setValue] = useState(""); 93 | if (!show) return null; 94 | return <div>{value}</div>; 95 | } 96 | ``` 97 | 98 | **Rule 2: Only call from React functions** 99 | ```tsx 100 | // ❌ WRONG - Hook in regular function 101 | function getUser() { 102 | const [user, setUser] = useState(null); // Not allowed! 103 | return user; 104 | } 105 | 106 | // ✅ CORRECT - Hook in component or custom hook 107 | function useUser() { 108 | const [user, setUser] = useState(null); 109 | return user; 110 | } 111 | 112 | function Component() { 113 | const user = useUser(); // OK 114 | return <div>{user?.name}</div>; 115 | } 116 | ``` 117 | 118 | **Why these rules exist:** React stores hook state in a sequential array tied to each component instance. The array index depends on call order. Conditional hooks break this indexing, causing state to be assigned to the wrong hooks. 119 | 120 | --- 121 | 122 | ## React State Management Patterns 123 | 124 | Fundamental patterns for managing state in React, from local component state to shared state across component trees. 125 | 126 | ### `useState` - Local State 127 | 128 | **Syntax:** `const [state, setState] = useState(initialValue)` 129 | 130 | ```tsx 131 | // Basic 132 | const [count, setCount] = useState(0); 133 | 134 | // Functional update (when new state depends on old) 135 | setCount(prev => prev + 1); 136 | 137 | // Lazy initialization (expensive initial state) 138 | const [data, setData] = useState(() => expensiveComputation()); 139 | 140 | // Immutable updates 141 | setUser(prev => ({ ...prev, name })); 142 | setItems(prev => [...prev, newItem]); 143 | ``` 144 | 145 | **Use when:** State is local, updates are simple, no complex transitions. 146 | **Consider alternatives:** Multiple components → Context, complex logic → useReducer. 147 | 148 | --- 149 | 150 | ### `useReducer` - Complex State Logic 151 | 152 | **Syntax:** `const [state, dispatch] = useReducer(reducer, initialState)` 153 | 154 | ```tsx 155 | type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' }; 156 | 157 | function reducer(state: State, action: Action): State { 158 | switch (action.type) { 159 | case 'increment': return { count: state.count + 1 }; 160 | case 'decrement': return { count: state.count - 1 }; 161 | case 'reset': return { count: 0 }; 162 | default: return state; 163 | } 164 | } 165 | 166 | const [state, dispatch] = useReducer(reducer, { count: 0 }); 167 | dispatch({ type: 'increment' }); 168 | ``` 169 | 170 | **With Immer (XMLUI pattern):** 171 | ```tsx 172 | import produce from 'immer'; 173 | 174 | const reducer = produce((draft, action) => { 175 | switch (action.type) { 176 | case 'ADD_TODO': 177 | draft.todos.push(action.payload); // Direct mutation 178 | break; 179 | } 180 | }); 181 | ``` 182 | 183 | **Use when:** Multiple related state values, complex transitions, want to separate state logic. 184 | **Use useState when:** Simple independent values, straightforward updates. 185 | 186 | --- 187 | 188 | ### Context API - Avoid Prop Drilling 189 | 190 | **Purpose:** Share state across component tree without passing props through every level. 191 | 192 | **Pattern:** Create context → Provider component → Custom hook → Consume 193 | 194 | ```tsx 195 | // 1. Create context 196 | const AuthContext = createContext<AuthContext | null>(null); 197 | 198 | // 2. Custom hook with validation 199 | function useAuth() { 200 | const context = useContext(AuthContext); 201 | if (!context) throw new Error('useAuth must be used within AuthProvider'); 202 | return context; 203 | } 204 | 205 | // 3. Provider component 206 | function AuthProvider({ children }: Props) { 207 | const [user, setUser] = useState<User | null>(null); 208 | 209 | const value = useMemo(() => ({ 210 | user, 211 | login: async (creds: Credentials) => { /* ... */ }, 212 | logout: () => setUser(null), 213 | }), [user]); 214 | 215 | return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; 216 | } 217 | 218 | // 4. Consume anywhere in tree 219 | function Component() { 220 | const { user, logout } = useAuth(); // No prop drilling! 221 | return <button onClick={logout}>{user.name}</button>; 222 | } 223 | ``` 224 | 225 | **XMLUI Example:** 226 | ```tsx 227 | // AppLayoutContext - Provides layout state to nested navigation 228 | const AppLayoutContext = createContext<IAppLayoutContext | null>(null); 229 | 230 | export const App = forwardRef(function App(props, ref) { 231 | const layoutContextValue = useMemo(() => ({ 232 | layout, 233 | navPanelVisible, 234 | toggleDrawer, 235 | }), [layout, navPanelVisible, toggleDrawer]); 236 | 237 | return ( 238 | <AppLayoutContext.Provider value={layoutContextValue}> 239 | {content} 240 | </AppLayoutContext.Provider> 241 | ); 242 | }); 243 | 244 | // NavLink accesses layout directly 245 | export const NavLink = forwardRef(function NavLink(props, ref) { 246 | const { layout } = useAppLayoutContext(); 247 | // No prop drilling! 248 | }); 249 | ``` 250 | 251 | **Use when:** Many nested components need access, >3 levels of prop drilling, global state (theme, auth). 252 | **Don't use when:** 1-2 levels of nesting (props fine), high-frequency updates (re-renders all consumers). 253 | 254 | **Performance tip:** Split contexts by update frequency - separate user/theme/settings contexts instead of one combined context. 255 | 256 | --- 257 | 258 | ### State Lifting 259 | 260 | **Pattern:** Move state to common ancestor to share between siblings. 261 | 262 | ```tsx 263 | // ❌ WRONG - Siblings can't communicate 264 | <Parent><InputA /><InputB /></Parent> 265 | 266 | // ✅ CORRECT - Lift state to parent 267 | function Parent() { 268 | const [value, setValue] = useState(''); 269 | return ( 270 | <> 271 | <InputA value={value} onChange={setValue} /> 272 | <InputB value={value} /> 273 | </> 274 | ); 275 | } 276 | ``` 277 | 278 | **XMLUI Pattern:** Container-based state flows down automatically: 279 | ```tsx 280 | <Stack var.selectedId="{null}"> 281 | <Button onClick={"{() => selectedId = 'item1'}" /> 282 | <Display value="{selectedId}" /> 283 | </Stack> 284 | ``` 285 | 286 | **Use when:** Siblings need to coordinate, parent orchestrates behavior. 287 | **Don't use when:** State only used by one component, excessive prop drilling (use context). 288 | 289 | --- 290 | 291 | ### Controlled vs Uncontrolled Components 292 | 293 | **Controlled:** Parent manages state via `value` prop. 294 | ```tsx 295 | function Controlled({ value, onChange }: Props) { 296 | return <input value={value} onChange={e => onChange(e.target.value)} />; 297 | } 298 | ``` 299 | 300 | **Uncontrolled:** Component manages own state via `initialValue`. 301 | ```tsx 302 | function Uncontrolled({ initialValue = '', onDidChange }: Props) { 303 | const [value, setValue] = useState(initialValue); 304 | return <input value={value} onChange={e => { setValue(e.target.value); onDidChange?.(e.target.value); }} />; 305 | } 306 | ``` 307 | 308 | **Hybrid (XMLUI pattern):** Support both modes. 309 | ```tsx 310 | function Flexible({ value, initialValue = '', onDidChange }: Props) { 311 | const [localValue, setLocalValue] = useState(initialValue); 312 | 313 | useEffect(() => { 314 | if (value !== undefined) setLocalValue(value); 315 | }, [value]); 316 | 317 | const handleChange = (e) => { 318 | setLocalValue(e.target.value); 319 | onDidChange?.(e.target.value); 320 | }; 321 | 322 | return <input value={localValue} onChange={handleChange} />; 323 | } 324 | ``` 325 | 326 | **Use controlled:** Validate/format input, value affects other UI, programmatic changes. 327 | **Use uncontrolled:** Simple forms (read on submit), performance critical. 328 | **Use hybrid:** Reusable component libraries. 329 | 330 | --- 331 | 332 | ### Compound Components 333 | 334 | **Purpose:** Components work together as cohesive unit, sharing state via context. 335 | 336 | ```tsx 337 | const TabsContext = createContext<{ 338 | activeTab: string; 339 | setActiveTab: (id: string) => void; 340 | } | null>(null); 341 | 342 | function Tabs({ children }: Props) { 343 | const [activeTab, setActiveTab] = useState('tab1'); 344 | return ( 345 | <TabsContext.Provider value={{ activeTab, setActiveTab }}> 346 | <div className="tabs">{children}</div> 347 | </TabsContext.Provider> 348 | ); 349 | } 350 | 351 | function Tab({ id, children }: Props) { 352 | const { activeTab, setActiveTab } = useContext(TabsContext)!; 353 | return ( 354 | <button 355 | className={activeTab === id ? 'active' : ''} 356 | onClick={() => setActiveTab(id)} 357 | > 358 | {children} 359 | </button> 360 | ); 361 | } 362 | 363 | Tabs.Tab = Tab; 364 | Tabs.Panel = TabPanel; 365 | 366 | // Usage - Flexible composition 367 | <Tabs> 368 | <Tabs.Tab id="tab1">First</Tabs.Tab> 369 | <Tabs.Tab id="tab2">Second</Tabs.Tab> 370 | <Tabs.Panel id="tab1">Content 1</Tabs.Panel> 371 | <Tabs.Panel id="tab2">Content 2</Tabs.Panel> 372 | </Tabs> 373 | ``` 374 | 375 | **Use when:** Tightly coupled components (tabs, accordion), need flexible composition, building libraries. 376 | **Don't use when:** Simple parent-child, no shared state, prop drilling is simple. 377 | 378 | --- 379 | 380 | ### Advanced Patterns 381 | 382 | **Provider Composition:** 383 | ```tsx 384 | function AppProviders({ children }: Props) { 385 | return ( 386 | <ThemeProvider> 387 | <AuthProvider> 388 | <RouterProvider> 389 | {children} 390 | </RouterProvider> 391 | </AuthProvider> 392 | </ThemeProvider> 393 | ); 394 | } 395 | ``` 396 | 397 | **Async Initialization:** 398 | ```tsx 399 | function AuthProvider({ children }: Props) { 400 | const [user, setUser] = useState(null); 401 | const [loading, setLoading] = useState(true); 402 | 403 | useEffect(() => { 404 | checkAuth().then(user => { setUser(user); setLoading(false); }); 405 | }, []); 406 | 407 | if (loading) return <LoadingScreen />; 408 | return <AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>; 409 | } 410 | ``` 411 | 412 | **Reducer with Immer (XMLUI):** 413 | ```tsx 414 | // StateContainer reducer 415 | export function createContainerReducer(debugView: IDebugViewContext) { 416 | return produce((state: ContainerState, action: ContainerAction) => { 417 | switch (action.type) { 418 | case ContainerActionKind.COMPONENT_STATE_CHANGED: 419 | state[uid] = { ...state[uid], ...action.payload.state }; 420 | break; 421 | } 422 | }); 423 | } 424 | ``` 425 | 426 | --- 427 | 428 | ## React Performance Optimization Patterns 429 | 430 | This section covers React's performance optimization tools and patterns. **Always profile before optimizing**—premature optimization adds complexity without real benefits. 431 | 432 | ### Core Optimization Hooks 433 | 434 | #### `useMemo` - Computation Caching 435 | 436 | Cache expensive calculations between renders. 437 | 438 | ```tsx 439 | const filtered = useMemo(() => 440 | items.filter(item => item.includes(filter)), 441 | [items, filter] 442 | ); 443 | ``` 444 | 445 | **Use when:** Computation is expensive (>10ms), creating objects/arrays for memoized children, or calculations in dependency arrays. 446 | **Don't use when:** Computation is cheap (<1ms), result used only once, or component rarely re-renders. 447 | 448 | #### `useCallback` - Function Caching 449 | 450 | Cache function definitions to prevent child re-renders. 451 | 452 | ```tsx 453 | const handleClick = useCallback(() => { 454 | doSomething(value); 455 | }, [value]); 456 | ``` 457 | 458 | **Use when:** Passing callbacks to memoized children or to dependency arrays. 459 | **Don't use when:** Function isn't passed to memoized components or deps arrays. 460 | 461 | **Note:** `useCallback(fn, deps)` is equivalent to `useMemo(() => fn, deps)`. 462 | 463 | #### `useTransition` - Non-Urgent Updates 464 | 465 | Mark state updates as low-priority to keep UI responsive. 466 | 467 | ```tsx 468 | const [isPending, startTransition] = useTransition(); 469 | 470 | startTransition(() => { 471 | setExpensiveState(newValue); // Won't block UI 472 | }); 473 | ``` 474 | 475 | **Use when:** Updating expensive state that doesn't need immediate feedback (filtering large lists, complex calculations). 476 | 477 | #### `memo` - Component Memoization 478 | 479 | Prevent re-renders when props haven't changed. 480 | 481 | ```tsx 482 | const MemoChild = memo(function MemoChild({ data }: Props) { 483 | return <div>{data.value}</div>; 484 | }); 485 | ``` 486 | 487 | **Use when:** Component renders frequently with same props, has expensive rendering, or is in large lists. 488 | **Don't use when:** Component rarely re-renders, props change every render, or rendering is cheap. 489 | 490 | **Important:** `memo` only works if props are stable. Use `useMemo`/`useCallback` for object/function props. 491 | 492 | --- 493 | 494 | ### Memoization Strategy Pattern 495 | 496 | **Principle:** `memo` + `useMemo` + `useCallback` work together. `memo` prevents re-renders, but only if props stay stable. Use `useMemo`/`useCallback` to keep props stable. 497 | 498 | #### The Memoization Cascade 499 | 500 | ```tsx 501 | // ❌ ANTI-PATTERN - memo without stable props 502 | const Child = memo(({ data, onClick }: Props) => <div onClick={onClick}>{data.value}</div>); 503 | 504 | function Parent() { 505 | return <Child data={{ value: 123 }} onClick={() => {}} />; // New refs every render! 506 | } 507 | 508 | // ✅ CORRECT - memo with stable props 509 | function Parent() { 510 | const data = useMemo(() => ({ value: 123 }), []); 511 | const onClick = useCallback(() => console.log('clicked'), []); 512 | return <Child data={data} onClick={onClick} />; 513 | } 514 | ``` 515 | 516 | #### Decision Tree 517 | 518 | 1. **Performance problem?** No → Don't optimize. Yes → Step 2. 519 | 2. **What's the cause?** 520 | - Parent re-renders often → Use `memo()` on child 521 | - Expensive computation → Use `useMemo()` on calculation 522 | - New function props → Use `useCallback()` on function 523 | 3. **Passing objects/arrays/functions to memoized component?** Yes → Memoize those too. 524 | 525 | #### Common Patterns 526 | 527 | ```tsx 528 | // Pattern 1: Context values 529 | const contextValue = useMemo(() => ({ 530 | state, 531 | setState, 532 | isLoading: state.status === 'loading', 533 | }), [state]); 534 | 535 | // Pattern 2: Event handlers with deps 536 | const handleSearch = useCallback(() => { 537 | if (query.length >= minLength) onSearch(query); 538 | }, [query, minLength, onSearch]); 539 | 540 | // Pattern 3: Expensive selectors 541 | const filteredData = useMemo(() => { 542 | return data.filter(item => item.name.includes(filter)).sort((a, b) => a.name.localeCompare(b.name)); 543 | }, [data, filter]); 544 | 545 | // Pattern 4: Derived state 546 | const summary = useMemo(() => ({ 547 | total: items.reduce((sum, item) => sum + item.price * item.quantity, 0), 548 | itemCount: items.reduce((sum, item) => sum + item.quantity, 0), 549 | }), [items]); 550 | ``` 551 | 552 | #### Anti-Patterns 553 | 554 | ```tsx 555 | // ❌ Over-memoization 556 | const greeting = useMemo(() => `Hello, ${name}`, [name]); // Too simple! 557 | 558 | // ❌ Incomplete chain 559 | <MemoChild config={{ theme: 'dark' }} />; // New object defeats memo 560 | 561 | // ❌ Unstable dependencies 562 | useMemo(() => formatUser(user), [user]); // user object recreated every render 563 | // ✅ Fix: useMemo(() => formatUser(user), [user.id, user.name]); 564 | ``` 565 | 566 | #### Checklist 567 | 568 | **✅ DO memoize:** 569 | - Components rendering frequently with same props 570 | - Expensive computations (>10ms) 571 | - Objects/arrays/functions passed to memoized children 572 | - Context values 573 | 574 | **❌ DON'T memoize:** 575 | - Cheap operations (<1ms) 576 | - Values that change every render 577 | - Without profiling first 578 | 579 | --- 580 | 581 | ### Virtualization Pattern 582 | 583 | **Purpose:** Render only visible items in large lists by using "windowing." Instead of rendering 10,000 items, render only ~10 visible items. 584 | 585 | **Libraries:** XMLUI uses two based on component needs: 586 | - **virtua** (Tree, List) - Chat interfaces, reverse scrolling, auto-sizing, fixed-size lists 587 | - **@tanstack/react-virtual** (Table) - Dynamic measurements, flexible 588 | 589 | **Library Comparison:** 590 | 591 | | Feature | virtua | @tanstack/react-virtual | 592 | |---------|--------|------------------------| 593 | | **Bundle Size** | ~6KB | ~4KB | 594 | | **API** | Render props | Hooks | 595 | | **Dynamic heights** | Automatic | Automatic | 596 | | **Reverse scroll** | ✅ Built-in | Manual | 597 | | **Auto-sizing** | ✅ Built-in | Manual | 598 | | **XMLUI Usage** | Tree, List | Table | 599 | 600 | **virtua Example (XMLUI List):** 601 | 602 | ```tsx 603 | import { Virtualizer } from 'virtua'; 604 | 605 | function ChatList({ messages }: Props) { 606 | return ( 607 | <Virtualizer count={messages.length}> 608 | {(index) => { 609 | const msg = messages[index]; 610 | return ( 611 | <div key={msg.id}> 612 | <div>{msg.author}</div> 613 | <div>{msg.content}</div> 614 | </div> 615 | ); 616 | }} 617 | </Virtualizer> 618 | ); 619 | } 620 | ``` 621 | 622 | **@tanstack/react-virtual Example:** 623 | 624 | ```tsx 625 | import { useVirtualizer } from '@tanstack/react-virtual'; 626 | 627 | function DataTable({ rows }: Props) { 628 | const tableRef = useRef<HTMLDivElement>(null); 629 | 630 | const rowVirtualizer = useVirtualizer({ 631 | count: rows.length, 632 | getScrollElement: () => tableRef.current, 633 | estimateSize: () => 30, 634 | overscan: 5, 635 | }); 636 | 637 | return ( 638 | <div ref={tableRef} style={{ height: '400px', overflow: 'auto' }}> 639 | {rowVirtualizer.getVirtualItems().map((virtualRow) => ( 640 | <div 641 | key={virtualRow.index} 642 | ref={(el) => rowVirtualizer.measureElement(el)} 643 | style={{ transform: `translateY(${virtualRow.start}px)` }} 644 | > 645 | {rows[virtualRow.index].content} 646 | </div> 647 | ))} 648 | </div> 649 | ); 650 | } 651 | ``` 652 | 653 | **Critical Rules:** 654 | 1. **Memoize row components** - Use `React.memo()` 655 | 2. **Apply transform/style** - Required for positioning (@tanstack) 656 | 3. **Memoize data** - Prevent row re-renders 657 | 4. **Handle scroll container** - Each library handles sizing differently 658 | 659 | **Performance Impact:** 660 | 661 | | Items | Normal | Virtualized | Improvement | 662 | |-------|--------|-------------|-------------| 663 | | 100 | 50ms | 10ms | 5x faster | 664 | | 1,000 | 500ms | 10ms | 50x faster | 665 | | 10,000 | 5s | 10ms | 500x faster | 666 | 667 | **When to Use:** 668 | - ✅ >100 items 669 | - ✅ Uniform item sizes 670 | - ✅ Scrollable datasets 671 | - ❌ <100 items (overhead not worth it) 672 | - ❌ Already paginated 673 | - ❌ Complex/unpredictable heights 674 | 675 | --- 676 | 677 | ### Rate Limiting: Debouncing and Throttling 678 | 679 | **Purpose:** Control the frequency of expensive operations during high-frequency events (user input, scrolling, resizing). 680 | 681 | **Key Difference:** 682 | - **Debouncing**: Wait until activity **stops** (search, autosave) 683 | - **Throttling**: Execute at **regular intervals** during activity (scroll, mousemove) 684 | 685 | ```tsx 686 | // User types "search" continuously 687 | 688 | // DEBOUNCING: Executes ONCE after user stops typing 689 | // Timeline: [type...type...type...STOP] → Execute 690 | 691 | // THROTTLING: Executes EVERY 200ms while typing 692 | // Timeline: Execute → [200ms] → Execute → [200ms] → Execute... 693 | ``` 694 | 695 | #### Debouncing Solutions 696 | 697 | **1. useDeferredValue (React 18+) - Recommended** 698 | 699 | ```tsx 700 | function Search() { 701 | const [query, setQuery] = useState(''); 702 | const deferredQuery = useDeferredValue(query); 703 | 704 | const results = useMemo(() => { 705 | if (deferredQuery.length < 2) return []; 706 | return performSearch(deferredQuery); 707 | }, [deferredQuery]); 708 | 709 | return ( 710 | <> 711 | <input value={query} onChange={e => setQuery(e.target.value)} /> 712 | <ResultsList results={results} /> 713 | </> 714 | ); 715 | } 716 | ``` 717 | 718 | **2. Custom useDebounce Hook** 719 | 720 | ```tsx 721 | function useDebounce<T>(value: T, delay: number = 500): T { 722 | const [debouncedValue, setDebouncedValue] = useState<T>(value); 723 | 724 | useEffect(() => { 725 | const timer = setTimeout(() => setDebouncedValue(value), delay); 726 | return () => clearTimeout(timer); 727 | }, [value, delay]); 728 | 729 | return debouncedValue; 730 | } 731 | 732 | // Usage 733 | const debouncedQuery = useDebounce(query, 300); 734 | ``` 735 | 736 | **3. Lodash debounce** 737 | 738 | ```tsx 739 | const debouncedSave = useMemo( 740 | () => debounce((text: string) => saveToServer(text), 1000), 741 | [] 742 | ); 743 | 744 | useEffect(() => { 745 | return () => debouncedSave.cancel(); 746 | }, [debouncedSave]); 747 | ``` 748 | 749 | #### Throttling Solutions 750 | 751 | **1. Custom useThrottle Hook** 752 | 753 | ```tsx 754 | function useThrottle<T extends (...args: any[]) => any>( 755 | callback: T, 756 | delay: number = 200 757 | ): T { 758 | const lastRun = useRef(Date.now()); 759 | 760 | return useCallback((...args: Parameters<T>) => { 761 | const now = Date.now(); 762 | if (now - lastRun.current >= delay) { 763 | lastRun.current = now; 764 | return callback(...args); 765 | } 766 | }, [callback, delay]) as T; 767 | } 768 | 769 | // Usage 770 | const handleScroll = useThrottle(() => { 771 | setScrollPos(window.scrollY); 772 | }, 200); 773 | ``` 774 | 775 | **2. Lodash throttle** 776 | 777 | ```tsx 778 | const throttledScroll = useMemo( 779 | () => throttle(() => { 780 | updateScrollPosition(); 781 | }, 200, { 782 | leading: true, // Execute on first call 783 | trailing: true // Execute after interval ends 784 | }), 785 | [] 786 | ); 787 | 788 | useEffect(() => { 789 | return () => throttledScroll.cancel(); 790 | }, [throttledScroll]); 791 | ``` 792 | 793 | #### XMLUI Examples 794 | 795 | **Debounced Search (Search.tsx):** 796 | ```tsx 797 | function Search({ data, limit }: Props) { 798 | const [inputValue, setInputValue] = useState(""); 799 | const debouncedValue = useDeferredValue(inputValue); 800 | 801 | const results = useMemo(() => { 802 | if (debouncedValue.length <= 1) return []; 803 | return fuse.search(debouncedValue, { limit }); 804 | }, [debouncedValue, limit]); 805 | 806 | return ( 807 | <> 808 | <input value={inputValue} onChange={e => setInputValue(e.target.value)} /> 809 | <SearchResults results={results} /> 810 | </> 811 | ); 812 | } 813 | ``` 814 | 815 | **Throttled Change Listener (ChangeListenerNative.tsx):** 816 | ```tsx 817 | function ChangeListener({ listenTo, onChange, throttleWaitInMs = 0 }: Props) { 818 | const throttledOnChange = useMemo(() => { 819 | if (throttleWaitInMs !== 0 && onChange) { 820 | return throttle(onChange, throttleWaitInMs, { leading: true }); 821 | } 822 | return onChange; 823 | }, [onChange, throttleWaitInMs]); 824 | 825 | useEffect(() => { 826 | if (throttledOnChange) { 827 | throttledOnChange({ prevValue, newValue: listenTo }); 828 | } 829 | }, [listenTo, throttledOnChange]); 830 | } 831 | ``` 832 | 833 | **Async Throttle for Validation (misc.ts):** 834 | ```tsx 835 | function asyncThrottle<F extends (...args: any[]) => Promise<any>>( 836 | func: F, 837 | wait?: number, 838 | options?: ThrottleSettings 839 | ) { 840 | const throttled = throttle( 841 | (resolve, reject, args: Parameters<F>) => { 842 | void func(...args).then(resolve).catch(reject); 843 | }, 844 | wait, 845 | options 846 | ); 847 | 848 | return (...args: Parameters<F>): ReturnType<F> => 849 | new Promise((resolve, reject) => { 850 | throttled(resolve, reject, args); 851 | }) as ReturnType<F>; 852 | } 853 | ``` 854 | 855 | #### Decision Guide 856 | 857 | | Scenario | Solution | Timing | 858 | |----------|----------|--------| 859 | | Search input | Debounce | 300ms | 860 | | Form validation | Debounce | 500ms | 861 | | Autosave | Debounce | 1000ms | 862 | | Scroll position | Throttle | 100-200ms | 863 | | Window resize | Throttle | 200-300ms | 864 | | Mouse tracking | Throttle | 50-100ms | 865 | | API rate limiting | Throttle | 500-1000ms | 866 | 867 | #### Performance Impact 868 | 869 | | Operation | Without | With (300ms) | Improvement | 870 | |-----------|---------|--------------|-------------| 871 | | Search (6 chars typed) | 6 API calls | 1 API call | 83% reduction | 872 | | Scroll (1s) | ~60 events | 5 events | 92% reduction | 873 | | Window resize | ~30 events | 5 events | 83% reduction | 874 | 875 | #### Critical Rules 876 | 877 | **1. Always memoize** rate-limited functions: 878 | ```tsx 879 | // ❌ WRONG - Creates new function every render 880 | const handle = debounce(() => search(), 500); 881 | 882 | // ✅ CORRECT - Memoized 883 | const handle = useMemo(() => debounce(() => search(), 500), []); 884 | ``` 885 | 886 | **2. Always cleanup**: 887 | ```tsx 888 | useEffect(() => { 889 | return () => debouncedFn.cancel(); 890 | }, [debouncedFn]); 891 | ``` 892 | 893 | **3. Don't rate-limit UI state** - only side effects: 894 | ```tsx 895 | // ❌ WRONG - UI lags 896 | const handleChange = debounce((e) => setValue(e.target.value), 300); 897 | 898 | // ✅ CORRECT - Immediate UI, debounced side effect 899 | const handleChange = (e) => { 900 | setValue(e.target.value); // Instant 901 | debouncedSearch(e.target.value); // Delayed 902 | }; 903 | ``` 904 | 905 | **4. Choose appropriate delays**: 906 | - Search: 300ms 907 | - Autosave: 1000ms 908 | - Scroll/resize: 100-200ms 909 | - Mousemove: 50ms 910 | 911 | #### When to Use 912 | 913 | **Debouncing:** 914 | - ✅ Search, autosave, validation 915 | - ✅ Wait for user to finish action 916 | - ❌ Don't use for immediate feedback 917 | 918 | **Throttling:** 919 | - ✅ Scroll, resize, mousemove 920 | - ✅ Execute during continuous activity 921 | - ❌ Don't use when only final value matters 922 | 923 | #### Resources 924 | 925 | - [useDeferredValue](https://react.dev/reference/react/useDeferredValue) - React 18 docs 926 | - [lodash.debounce](https://lodash.com/docs/#debounce) - Debounce docs 927 | - [lodash.throttle](https://lodash.com/docs/#throttle) - Throttle docs 928 | - [XMLUI Search](packages/xmlui-search/src/Search.tsx) - Production example 929 | 930 | --- 931 | 932 | ## React Event Handling Patterns 933 | 934 | Patterns for handling user interactions efficiently and correctly in React applications. 935 | 936 | ### Event Delegation Pattern 937 | 938 | **Purpose:** Handle events for multiple children at parent level instead of attaching handlers to each child. 939 | 940 | **Benefits:** 941 | - Fewer event listeners (better memory usage) 942 | - Works with dynamically added/removed children 943 | - Simplifies event handler management 944 | 945 | ```tsx 946 | // ❌ ANTI-PATTERN - Handler on every item 947 | function List({ items }: Props) { 948 | return ( 949 | <ul> 950 | {items.map(item => ( 951 | <li key={item.id} onClick={() => handleClick(item.id)}> 952 | {item.name} 953 | </li> 954 | ))} 955 | </ul> 956 | ); 957 | } 958 | 959 | // ✅ CORRECT - Single handler on parent 960 | function List({ items }: Props) { 961 | const handleClick = (e: React.MouseEvent<HTMLUListElement>) => { 962 | const target = e.target as HTMLElement; 963 | const li = target.closest('li'); 964 | if (li) { 965 | const itemId = li.dataset.id; 966 | console.log('Clicked item:', itemId); 967 | } 968 | }; 969 | 970 | return ( 971 | <ul onClick={handleClick}> 972 | {items.map(item => ( 973 | <li key={item.id} data-id={item.id}> 974 | {item.name} 975 | </li> 976 | ))} 977 | </ul> 978 | ); 979 | } 980 | ``` 981 | 982 | **XMLUI Example (Tree component):** 983 | ```tsx 984 | function Tree({ items }: Props) { 985 | // Single click handler for entire tree 986 | const handleTreeClick = useCallback((e: React.MouseEvent) => { 987 | const target = e.target as HTMLElement; 988 | const treeItem = target.closest('[data-tree-item]'); 989 | 990 | if (treeItem) { 991 | const itemId = treeItem.getAttribute('data-item-id'); 992 | const action = target.getAttribute('data-action'); 993 | 994 | if (action === 'expand') { 995 | toggleExpand(itemId); 996 | } else if (action === 'select') { 997 | selectItem(itemId); 998 | } 999 | } 1000 | }, [toggleExpand, selectItem]); 1001 | 1002 | return ( 1003 | <div className="tree" onClick={handleTreeClick}> 1004 | {renderTreeItems(items)} 1005 | </div> 1006 | ); 1007 | } 1008 | ``` 1009 | 1010 | **When to use:** 1011 | - Lists with many items (>50) 1012 | - Dynamic children (added/removed frequently) 1013 | - Multiple event types on same children 1014 | - Performance-critical rendering 1015 | 1016 | **When NOT to use:** 1017 | - Few items (<10) - overhead not worth it 1018 | - Need precise event target info 1019 | - Event handler logic is complex per-item 1020 | 1021 | --- 1022 | 1023 | ### Synthetic Event Pattern 1024 | 1025 | **Purpose:** React wraps native browser events in `SyntheticEvent` for cross-browser consistency. 1026 | 1027 | **Key differences from native events:** 1028 | 1029 | | Feature | Native Event | Synthetic Event | 1030 | |---------|-------------|-----------------| 1031 | | **Type** | Browser-specific | Unified React type | 1032 | | **Pooling (React 16)** | No | Yes (reused) | 1033 | | **Pooling (React 17+)** | No | No (deprecated) | 1034 | | **Properties** | Browser-specific | Normalized | 1035 | | **Access after handler** | ✅ Always available | ⚠️ Nullified (React 16 only) | 1036 | 1037 | **Basic usage:** 1038 | ```tsx 1039 | function Input() { 1040 | const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { 1041 | // e is SyntheticEvent, not native Event 1042 | console.log(e.target.value); // ✅ Works 1043 | console.log(e.currentTarget); // ✅ Works 1044 | 1045 | // Access native event if needed 1046 | const nativeEvent = e.nativeEvent; 1047 | }; 1048 | 1049 | return <input onChange={handleChange} />; 1050 | } 1051 | ``` 1052 | 1053 | **Event pooling (React 16 only):** 1054 | ```tsx 1055 | // ❌ WRONG - Async access (React 16) 1056 | function Bad() { 1057 | const handleClick = (e: React.MouseEvent) => { 1058 | setTimeout(() => { 1059 | console.log(e.target); // null in React 16! 1060 | }, 1000); 1061 | }; 1062 | return <button onClick={handleClick}>Click</button>; 1063 | } 1064 | 1065 | // ✅ CORRECT - Persist event (React 16) 1066 | function Good() { 1067 | const handleClick = (e: React.MouseEvent) => { 1068 | e.persist(); // Keep event alive 1069 | setTimeout(() => { 1070 | console.log(e.target); // ✅ Works 1071 | }, 1000); 1072 | }; 1073 | return <button onClick={handleClick}>Click</button>; 1074 | } 1075 | 1076 | // ✅ BETTER - Extract values (React 16 & 17+) 1077 | function Better() { 1078 | const handleClick = (e: React.MouseEvent) => { 1079 | const target = e.target; // Capture immediately 1080 | setTimeout(() => { 1081 | console.log(target); // ✅ Works in all versions 1082 | }, 1000); 1083 | }; 1084 | return <button onClick={handleClick}>Click</button>; 1085 | } 1086 | ``` 1087 | 1088 | **Note:** React 17+ removed event pooling, so `e.persist()` is no longer needed. 1089 | 1090 | **Common event types:** 1091 | ```tsx 1092 | // Mouse events 1093 | const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {}; 1094 | const handleDoubleClick = (e: React.MouseEvent) => {}; 1095 | 1096 | // Keyboard events 1097 | const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { 1098 | if (e.key === 'Enter') submitForm(); 1099 | }; 1100 | 1101 | // Form events 1102 | const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {}; 1103 | const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { 1104 | e.preventDefault(); 1105 | }; 1106 | 1107 | // Focus events 1108 | const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {}; 1109 | const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {}; 1110 | 1111 | // Clipboard events 1112 | const handleCopy = (e: React.ClipboardEvent) => {}; 1113 | const handlePaste = (e: React.ClipboardEvent) => { 1114 | const text = e.clipboardData.getData('text'); 1115 | }; 1116 | 1117 | // Drag events 1118 | const handleDragStart = (e: React.DragEvent) => {}; 1119 | const handleDrop = (e: React.DragEvent) => { 1120 | e.preventDefault(); 1121 | const data = e.dataTransfer.getData('text'); 1122 | }; 1123 | ``` 1124 | 1125 | **Accessing native event:** 1126 | ```tsx 1127 | function Component() { 1128 | const handleClick = (e: React.MouseEvent) => { 1129 | // React synthetic event 1130 | console.log(e.currentTarget); // Element handler is attached to 1131 | console.log(e.target); // Element that triggered event 1132 | 1133 | // Native browser event 1134 | const nativeEvent = e.nativeEvent; 1135 | console.log(nativeEvent); // MouseEvent object 1136 | }; 1137 | 1138 | return <button onClick={handleClick}>Click</button>; 1139 | } 1140 | ``` 1141 | 1142 | --- 1143 | 1144 | ### Event Callback Composition Pattern 1145 | 1146 | **Purpose:** Combine multiple event handlers into a single handler, useful for library components that accept user callbacks. 1147 | 1148 | **Pattern 1: Sequential execution** 1149 | ```tsx 1150 | function composeHandlers<E>(...handlers: Array<((e: E) => void) | undefined>) { 1151 | return (event: E) => { 1152 | handlers.forEach(handler => { 1153 | if (handler) { 1154 | handler(event); 1155 | } 1156 | }); 1157 | }; 1158 | } 1159 | 1160 | // Usage 1161 | function Button({ onClick, onClickInternal }: Props) { 1162 | const handleClick = composeHandlers(onClickInternal, onClick); 1163 | return <button onClick={handleClick}>Click</button>; 1164 | } 1165 | ``` 1166 | 1167 | **Pattern 2: Conditional execution (stop on preventDefault)** 1168 | ```tsx 1169 | function composeEventHandlers<E extends React.SyntheticEvent>( 1170 | internalHandler?: (e: E) => void, 1171 | externalHandler?: (e: E) => void 1172 | ) { 1173 | return (event: E) => { 1174 | internalHandler?.(event); 1175 | 1176 | // If internal handler called preventDefault, stop 1177 | if (!event.defaultPrevented) { 1178 | externalHandler?.(event); 1179 | } 1180 | }; 1181 | } 1182 | 1183 | // Usage - XMLUI pattern 1184 | function Select({ onChange, onDidChange }: Props) { 1185 | const handleInternalChange = (e: React.ChangeEvent<HTMLSelectElement>) => { 1186 | // Internal logic (validation, state updates) 1187 | validateValue(e.target.value); 1188 | 1189 | // Prevent external handler if validation fails 1190 | if (!isValid) { 1191 | e.preventDefault(); 1192 | } 1193 | }; 1194 | 1195 | const handleChange = composeEventHandlers(handleInternalChange, onChange); 1196 | 1197 | return <select onChange={handleChange}>...</select>; 1198 | } 1199 | ``` 1200 | 1201 | **Pattern 3: Merge handlers from props** 1202 | ```tsx 1203 | function mergeEventHandlers<T extends React.SyntheticEvent>( 1204 | ours: ((e: T) => void) | undefined, 1205 | theirs: ((e: T) => void) | undefined 1206 | ): ((e: T) => void) | undefined { 1207 | if (!ours) return theirs; 1208 | if (!theirs) return ours; 1209 | 1210 | return (event: T) => { 1211 | ours(event); 1212 | if (!event.defaultPrevented) { 1213 | theirs(event); 1214 | } 1215 | }; 1216 | } 1217 | 1218 | // Usage - Wrapper component 1219 | function Wrapper({ children, onClick }: Props) { 1220 | const internalClick = (e: React.MouseEvent) => { 1221 | console.log('Wrapper clicked'); 1222 | }; 1223 | 1224 | return cloneElement(children, { 1225 | onClick: mergeEventHandlers(internalClick, children.props.onClick), 1226 | }); 1227 | } 1228 | ``` 1229 | 1230 | **XMLUI Example (Container component):** 1231 | ```tsx 1232 | function Container({ children, ...props }: Props, ref: Ref<HTMLElement>) { 1233 | const renderedChild = renderChild(children); 1234 | 1235 | if (ref && renderedChild && isValidElement(renderedChild)) { 1236 | // Merge event handlers from both Container and child 1237 | const mergedProps = { 1238 | ...renderedChild.props, 1239 | onClick: composeEventHandlers(props.onClick, renderedChild.props.onClick), 1240 | onKeyDown: composeEventHandlers(props.onKeyDown, renderedChild.props.onKeyDown), 1241 | ref: composeRefs(ref, (renderedChild as any).ref), 1242 | }; 1243 | 1244 | return cloneElement(renderedChild, mergedProps); 1245 | } 1246 | 1247 | return renderedChild; 1248 | } 1249 | ``` 1250 | 1251 | **Pattern 4: Callback with additional args** 1252 | ```tsx 1253 | function withArgs<E, T>( 1254 | handler: ((e: E, ...args: T[]) => void) | undefined, 1255 | ...args: T[] 1256 | ) { 1257 | if (!handler) return undefined; 1258 | 1259 | return (event: E) => { 1260 | handler(event, ...args); 1261 | }; 1262 | } 1263 | 1264 | // Usage 1265 | function List({ items, onItemClick }: Props) { 1266 | return ( 1267 | <ul> 1268 | {items.map(item => ( 1269 | <li key={item.id} onClick={withArgs(onItemClick, item.id)}> 1270 | {item.name} 1271 | </li> 1272 | ))} 1273 | </ul> 1274 | ); 1275 | } 1276 | ``` 1277 | 1278 | **When to compose handlers:** 1279 | - Building reusable component libraries 1280 | - Wrapper components that add behavior 1281 | - Components with internal + external handlers 1282 | - Need to call parent handler conditionally 1283 | 1284 | **Best practices:** 1285 | - Always check if handler exists before calling 1286 | - Respect `preventDefault()` and `stopPropagation()` 1287 | - Execute internal handlers first 1288 | - Document composition order clearly 1289 | 1290 | --- 1291 | 1292 | ## React Lifecycle and Effect Patterns 1293 | 1294 | Patterns for managing side effects, synchronization, and component lifecycle in React applications. 1295 | 1296 | ### `useEffect` - Side Effects and Lifecycle 1297 | 1298 | **Purpose:** Run side effects after render (data fetching, subscriptions, DOM manipulation). 1299 | 1300 | **Syntax:** `useEffect(() => { /* effect */ return () => { /* cleanup */ } }, [dependencies])` 1301 | 1302 | **Basic usage:** 1303 | ```tsx 1304 | function UserProfile({ userId }: Props) { 1305 | const [user, setUser] = useState(null); 1306 | 1307 | useEffect(() => { 1308 | // Effect runs after render 1309 | fetch(`/api/users/${userId}`) 1310 | .then(res => res.json()) 1311 | .then(data => setUser(data)); 1312 | }, [userId]); // Re-run when userId changes 1313 | 1314 | return <div>{user?.name}</div>; 1315 | } 1316 | ``` 1317 | 1318 | **With async/await:** 1319 | ```tsx 1320 | useEffect(() => { 1321 | // Can't make callback async directly, use IIFE 1322 | (async () => { 1323 | const res = await fetch(`/api/users/${userId}`); 1324 | const data = await res.json(); 1325 | setUser(data); 1326 | })(); 1327 | }, [userId]); 1328 | ``` 1329 | 1330 | **Use when:** Data fetching, subscriptions, event listeners, DOM manipulation, integrating with non-React libraries. 1331 | **Avoid when:** Computing derived values (use `useMemo`), handling events (use handlers), initializing state (use initializer). 1332 | 1333 | --- 1334 | 1335 | ### Effect Cleanup Pattern 1336 | 1337 | **Purpose:** Properly clean up subscriptions, timers, and event listeners to prevent memory leaks. 1338 | 1339 | **Pattern: Always return cleanup function for subscriptions** 1340 | ```tsx 1341 | function Chat({ roomId }: Props) { 1342 | useEffect(() => { 1343 | const connection = createConnection(roomId); 1344 | connection.connect(); 1345 | 1346 | // ✅ Cleanup runs before next effect and on unmount 1347 | return () => { 1348 | connection.disconnect(); 1349 | }; 1350 | }, [roomId]); 1351 | 1352 | return <div>Connected to {roomId}</div>; 1353 | } 1354 | ``` 1355 | 1356 | **Pattern: Cancel async operations** 1357 | ```tsx 1358 | function DataComponent({ url }: Props) { 1359 | const [data, setData] = useState(null); 1360 | 1361 | useEffect(() => { 1362 | let cancelled = false; 1363 | 1364 | fetch(url) 1365 | .then(res => res.json()) 1366 | .then(data => { 1367 | if (!cancelled) setData(data); 1368 | }); 1369 | 1370 | // ✅ Prevent state updates after unmount 1371 | return () => { cancelled = true; }; 1372 | }, [url]); 1373 | 1374 | return <div>{JSON.stringify(data)}</div>; 1375 | } 1376 | ``` 1377 | 1378 | **Pattern: Remove event listeners** 1379 | ```tsx 1380 | function WindowSize() { 1381 | const [size, setSize] = useState({ width: 0, height: 0 }); 1382 | 1383 | useEffect(() => { 1384 | const handleResize = () => { 1385 | setSize({ width: window.innerWidth, height: window.innerHeight }); 1386 | }; 1387 | 1388 | window.addEventListener('resize', handleResize); 1389 | handleResize(); // Initial size 1390 | 1391 | // ✅ Always remove listeners 1392 | return () => window.removeEventListener('resize', handleResize); 1393 | }, []); 1394 | 1395 | return <div>{size.width} x {size.height}</div>; 1396 | } 1397 | ``` 1398 | 1399 | **Pattern: Clear timers and intervals** 1400 | ```tsx 1401 | function Timer() { 1402 | const [count, setCount] = useState(0); 1403 | 1404 | useEffect(() => { 1405 | const interval = setInterval(() => { 1406 | setCount(c => c + 1); 1407 | }, 1000); 1408 | 1409 | // ✅ Clear interval on unmount 1410 | return () => clearInterval(interval); 1411 | }, []); 1412 | 1413 | return <div>{count}s</div>; 1414 | } 1415 | ``` 1416 | 1417 | **Pattern: Unsubscribe from external stores** 1418 | ```tsx 1419 | function ExternalStore({ store }: Props) { 1420 | const [value, setValue] = useState(store.getValue()); 1421 | 1422 | useEffect(() => { 1423 | const unsubscribe = store.subscribe(newValue => { 1424 | setValue(newValue); 1425 | }); 1426 | 1427 | // ✅ Unsubscribe when component unmounts 1428 | return unsubscribe; 1429 | }, [store]); 1430 | 1431 | return <div>{value}</div>; 1432 | } 1433 | ``` 1434 | 1435 | **Critical rules:** 1436 | - Return cleanup for subscriptions, listeners, timers 1437 | - Use cancellation flags for async operations 1438 | - Cleanup runs before next effect and on unmount 1439 | - Don't forget to remove event listeners 1440 | 1441 | --- 1442 | 1443 | ### Effect Dependencies Pattern 1444 | 1445 | **Purpose:** Correctly manage dependency arrays to avoid stale closures and unnecessary re-runs. 1446 | 1447 | **Anti-pattern: Missing dependencies (stale closure bug)** 1448 | ```tsx 1449 | // ❌ WRONG - Stale closure 1450 | function Counter() { 1451 | const [count, setCount] = useState(0); 1452 | 1453 | useEffect(() => { 1454 | setInterval(() => { 1455 | console.log(count); // Always logs 0! 1456 | }, 1000); 1457 | }, []); // Missing count 1458 | 1459 | return <button onClick={() => setCount(count + 1)}>Increment</button>; 1460 | } 1461 | 1462 | // ✅ CORRECT - Use functional update 1463 | useEffect(() => { 1464 | setInterval(() => { 1465 | setCount(c => c + 1); // Uses latest value 1466 | }, 1000); 1467 | }, []); // No dependencies needed 1468 | ``` 1469 | 1470 | **Anti-pattern: Object/array in dependencies** 1471 | ```tsx 1472 | // ❌ WRONG - Object recreated every render 1473 | function Component({ config }: Props) { 1474 | useEffect(() => { 1475 | fetchData(config); 1476 | }, [config]); // Runs every render if config is new object 1477 | } 1478 | 1479 | // ✅ CORRECT - Destructure primitive values 1480 | useEffect(() => { 1481 | fetchData(config); 1482 | }, [config.id, config.filter]); // Only re-run when these change 1483 | ``` 1484 | 1485 | **Pattern: Empty array = run once on mount** 1486 | ```tsx 1487 | useEffect(() => { 1488 | // Initialization logic 1489 | initializeApp(); 1490 | 1491 | return () => { 1492 | // Cleanup on unmount 1493 | cleanupApp(); 1494 | }; 1495 | }, []); // Runs once on mount, cleanup on unmount 1496 | ``` 1497 | 1498 | **Pattern: No array = run after every render** 1499 | ```tsx 1500 | useEffect(() => { 1501 | // Runs after every render (rarely needed) 1502 | updateDocumentTitle(`Page - ${count}`); 1503 | }); // No dependency array 1504 | ``` 1505 | 1506 | **Pattern: Avoid callback dependencies with useRef** 1507 | ```tsx 1508 | function Component({ callback }: Props) { 1509 | const callbackRef = useRef(callback); 1510 | 1511 | // Keep ref updated 1512 | useEffect(() => { 1513 | callbackRef.current = callback; 1514 | }, [callback]); 1515 | 1516 | useEffect(() => { 1517 | const interval = setInterval(() => { 1518 | // ✅ Always uses latest callback 1519 | callbackRef.current(); 1520 | }, 1000); 1521 | 1522 | return () => clearInterval(interval); 1523 | }, []); // No callback in deps 1524 | } 1525 | ``` 1526 | 1527 | **Common mistakes:** 1528 | ```tsx 1529 | // ❌ Wrong - Missing dependencies 1530 | useEffect(() => { 1531 | doSomething(prop); // prop not in deps 1532 | }, []); 1533 | 1534 | // ❌ Wrong - Object identity 1535 | useEffect(() => { 1536 | fetchData(user); 1537 | }, [user]); // user object changes every render 1538 | 1539 | // ❌ Wrong - Function identity 1540 | useEffect(() => { 1541 | callback(); 1542 | }, [callback]); // callback recreated every render 1543 | 1544 | // ✅ Correct - Destructure objects 1545 | useEffect(() => { 1546 | fetchData({ id: user.id, name: user.name }); 1547 | }, [user.id, user.name]); 1548 | 1549 | // ✅ Correct - Memoize callbacks 1550 | const memoizedCallback = useCallback(callback, [dep]); 1551 | useEffect(() => { 1552 | memoizedCallback(); 1553 | }, [memoizedCallback]); 1554 | ``` 1555 | 1556 | **ESLint rule:** Always enable `react-hooks/exhaustive-deps` to catch dependency issues. 1557 | 1558 | --- 1559 | 1560 | ### Layout Effect Pattern 1561 | 1562 | **Purpose:** Run effects synchronously after DOM mutations but before browser paint to prevent visual flickering. 1563 | 1564 | **When to use `useLayoutEffect`:** 1565 | 1566 | **Pattern: DOM measurements before paint** 1567 | ```tsx 1568 | function Tooltip() { 1569 | const [position, setPosition] = useState({ x: 0, y: 0 }); 1570 | const ref = useRef<HTMLDivElement>(null); 1571 | 1572 | // ✅ CORRECT - Measure before paint 1573 | useLayoutEffect(() => { 1574 | if (ref.current) { 1575 | const rect = ref.current.getBoundingClientRect(); 1576 | setPosition({ 1577 | x: rect.left + rect.width / 2, 1578 | y: rect.top - 10, 1579 | }); 1580 | } 1581 | }, []); 1582 | 1583 | return <div ref={ref} style={{ left: position.x, top: position.y }}>Tooltip</div>; 1584 | } 1585 | 1586 | // ❌ WRONG - useEffect causes visible flicker 1587 | useEffect(() => { 1588 | // DOM measurements happen AFTER paint 1589 | // User sees element jump from old to new position 1590 | }, []); 1591 | ``` 1592 | 1593 | **Pattern: Synchronize scroll position** 1594 | ```tsx 1595 | function ScrollSync({ targetRef }: Props) { 1596 | useLayoutEffect(() => { 1597 | if (targetRef.current) { 1598 | // ✅ Scroll before paint, no flicker 1599 | targetRef.current.scrollTop = savedPosition; 1600 | } 1601 | }, [targetRef, savedPosition]); 1602 | } 1603 | ``` 1604 | 1605 | **Pattern: Prevent layout shift** 1606 | ```tsx 1607 | function AutoResizeTextarea({ value }: Props) { 1608 | const ref = useRef<HTMLTextAreaElement>(null); 1609 | 1610 | useLayoutEffect(() => { 1611 | if (ref.current) { 1612 | // ✅ Adjust height before paint 1613 | ref.current.style.height = 'auto'; 1614 | ref.current.style.height = `${ref.current.scrollHeight}px`; 1615 | } 1616 | }, [value]); 1617 | 1618 | return <textarea ref={ref} value={value} />; 1619 | } 1620 | ``` 1621 | 1622 | **Pattern: Third-party DOM library integration** 1623 | ```tsx 1624 | function Chart({ data }: Props) { 1625 | const containerRef = useRef<HTMLDivElement>(null); 1626 | 1627 | useLayoutEffect(() => { 1628 | if (containerRef.current) { 1629 | // ✅ Initialize library before paint 1630 | const chart = new ChartLibrary(containerRef.current); 1631 | chart.render(data); 1632 | 1633 | return () => chart.destroy(); 1634 | } 1635 | }, [data]); 1636 | 1637 | return <div ref={containerRef} />; 1638 | } 1639 | ``` 1640 | 1641 | **Comparison: useEffect vs useLayoutEffect** 1642 | 1643 | | Aspect | `useEffect` | `useLayoutEffect` | 1644 | |--------|------------|-------------------| 1645 | | **Timing** | After paint (async) | Before paint (sync) | 1646 | | **Blocks rendering** | ❌ No | ✅ Yes | 1647 | | **Use for** | Data fetching, subscriptions | DOM measurements, preventing flicker | 1648 | | **Performance** | Better (non-blocking) | Worse (blocks paint) | 1649 | | **SSR** | ✅ Works | ⚠️ Warning (no DOM on server) | 1650 | 1651 | **When to use each:** 1652 | - **useEffect**: 99% of cases - data fetching, subscriptions, analytics 1653 | - **useLayoutEffect**: DOM measurements, scroll sync, preventing visual flicker 1654 | 1655 | **Warning:** `useLayoutEffect` blocks visual updates. Only use when you need synchronous DOM access before paint. 1656 | 1657 | **SSR consideration:** 1658 | ```tsx 1659 | // ⚠️ useLayoutEffect doesn't run on server 1660 | useLayoutEffect(() => { 1661 | // This code only runs in browser 1662 | measureDOM(); 1663 | }, []); 1664 | 1665 | // ✅ Better: Use useEffect for SSR-compatible code 1666 | useEffect(() => { 1667 | measureDOM(); 1668 | }, []); 1669 | ``` 1670 | 1671 | --- 1672 | 1673 | ### Insertion Effect Pattern 1674 | 1675 | **Purpose:** Insert styles into DOM before layout effects run. Used by CSS-in-JS libraries. 1676 | 1677 | **Syntax:** `useInsertionEffect(() => { /* insert styles */ }, [dependencies])` 1678 | 1679 | **Effect execution order:** 1680 | 1. `useInsertionEffect` - Insert styles 1681 | 2. `useLayoutEffect` - Measure layout (reads styles) 1682 | 3. Browser paints 1683 | 4. `useEffect` - Other side effects 1684 | 1685 | **Pattern: CSS-in-JS style injection** 1686 | ```tsx 1687 | function useCSS(rule: string) { 1688 | useInsertionEffect(() => { 1689 | // ✅ Inject styles before layout reads 1690 | const style = document.createElement('style'); 1691 | style.textContent = rule; 1692 | document.head.appendChild(style); 1693 | 1694 | return () => document.head.removeChild(style); 1695 | }, [rule]); 1696 | } 1697 | 1698 | // Usage 1699 | function Button({ color }: Props) { 1700 | useCSS(` 1701 | .button-${color} { 1702 | background: ${color}; 1703 | border: 1px solid ${darken(color)}; 1704 | } 1705 | `); 1706 | 1707 | return <button className={`button-${color}`}>Click</button>; 1708 | } 1709 | ``` 1710 | 1711 | **Pattern: Dynamic theme injection (XMLUI)** 1712 | ```tsx 1713 | function ThemeProvider({ theme, children }: Props) { 1714 | useInsertionEffect(() => { 1715 | // ✅ Insert theme CSS before components measure 1716 | const styleElement = document.createElement('style'); 1717 | styleElement.id = 'theme-styles'; 1718 | styleElement.textContent = generateThemeCSS(theme); 1719 | document.head.appendChild(styleElement); 1720 | 1721 | return () => { 1722 | document.getElementById('theme-styles')?.remove(); 1723 | }; 1724 | }, [theme]); 1725 | 1726 | return children; 1727 | } 1728 | ``` 1729 | 1730 | **Pattern: Critical CSS injection** 1731 | ```tsx 1732 | function useCriticalCSS(css: string) { 1733 | useInsertionEffect(() => { 1734 | // ✅ Inject before any layout calculations 1735 | const style = document.createElement('style'); 1736 | style.setAttribute('data-critical', 'true'); 1737 | style.textContent = css; 1738 | document.head.insertBefore(style, document.head.firstChild); 1739 | 1740 | return () => style.remove(); 1741 | }, [css]); 1742 | } 1743 | ``` 1744 | 1745 | **When to use:** 1746 | - Building CSS-in-JS libraries 1747 | - Dynamic style generation 1748 | - Theme system implementation 1749 | - Critical CSS injection 1750 | 1751 | **When NOT to use:** 1752 | - Regular application code (use `useEffect`) 1753 | - Static stylesheets (use `<link>` tags) 1754 | - Non-style DOM manipulation 1755 | 1756 | **Note:** Rarely used directly in application code. Primarily for library authors. XMLUI uses this in `StyleContext` for theme style injection. 1757 | 1758 | **Comparison with other effects:** 1759 | 1760 | ```tsx 1761 | // ❌ WRONG - useEffect runs too late 1762 | useEffect(() => { 1763 | injectStyles(); // Styles added after layout measured 1764 | }, []); 1765 | 1766 | // ❌ WRONG - useLayoutEffect causes double layout 1767 | useLayoutEffect(() => { 1768 | injectStyles(); // Layout measured, then styles added, then re-measured 1769 | }, []); 1770 | 1771 | // ✅ CORRECT - useInsertionEffect runs first 1772 | useInsertionEffect(() => { 1773 | injectStyles(); // Styles ready before any layout measurement 1774 | }, []); 1775 | ``` 1776 | 1777 | --- 1778 | 1779 | ### Effect Best Practices Summary 1780 | 1781 | **1. Always clean up:** 1782 | ```tsx 1783 | useEffect(() => { 1784 | const subscription = subscribe(); 1785 | return () => subscription.unsubscribe(); // ✅ Cleanup 1786 | }, []); 1787 | ``` 1788 | 1789 | **2. Handle dependencies correctly:** 1790 | ```tsx 1791 | // ✅ Include all dependencies 1792 | useEffect(() => { 1793 | doSomething(prop, state); 1794 | }, [prop, state]); 1795 | 1796 | // ✅ Or use functional updates 1797 | useEffect(() => { 1798 | setState(prev => prev + 1); 1799 | }, []); // No state dependency needed 1800 | ``` 1801 | 1802 | **3. Choose the right effect hook:** 1803 | - `useEffect` - Default choice (async, after paint) 1804 | - `useLayoutEffect` - DOM measurements, prevent flicker (sync, before paint) 1805 | - `useInsertionEffect` - CSS-in-JS only (before layout) 1806 | 1807 | **4. Avoid common pitfalls:** 1808 | ```tsx 1809 | // ❌ Don't use objects in deps 1810 | useEffect(() => {}, [config]); // Runs every render 1811 | 1812 | // ✅ Destructure primitive values 1813 | useEffect(() => {}, [config.id, config.name]); 1814 | 1815 | // ❌ Don't make effect callback async 1816 | useEffect(async () => {}, []); // Type error 1817 | 1818 | // ✅ Use IIFE for async 1819 | useEffect(() => { 1820 | (async () => await fetch())(); 1821 | }, []); 1822 | ``` 1823 | 1824 | **5. Profile before optimizing:** 1825 | - Most effects are cheap 1826 | - Don't prematurely optimize with `useLayoutEffect` 1827 | - Measure actual performance impact 1828 | 1829 | --- 1830 | 1831 | ## `useRef` - Persistent Mutable References 1832 | 1833 | **Purpose:** Store mutable values that persist across renders without triggering re-renders. 1834 | 1835 | **Syntax:** `const ref = useRef(initialValue)` 1836 | 1837 | ### DOM References 1838 | 1839 | ```tsx 1840 | function TextInput() { 1841 | const inputRef = useRef<HTMLInputElement>(null); 1842 | 1843 | const focusInput = () => { 1844 | inputRef.current?.focus(); 1845 | }; 1846 | 1847 | return ( 1848 | <div> 1849 | <input ref={inputRef} /> 1850 | <button onClick={focusInput}>Focus</button> 1851 | </div> 1852 | ); 1853 | } 1854 | ``` 1855 | 1856 | ### Storing Mutable Values 1857 | 1858 | ```tsx 1859 | function Timer() { 1860 | const [count, setCount] = useState(0); 1861 | const intervalRef = useRef<NodeJS.Timeout>(); 1862 | 1863 | useEffect(() => { 1864 | intervalRef.current = setInterval(() => { 1865 | setCount(c => c + 1); 1866 | }, 1000); 1867 | 1868 | return () => clearInterval(intervalRef.current); 1869 | }, []); 1870 | 1871 | const stop = () => { 1872 | clearInterval(intervalRef.current); 1873 | }; 1874 | 1875 | return ( 1876 | <div> 1877 | {count} seconds 1878 | <button onClick={stop}>Stop</button> 1879 | </div> 1880 | ); 1881 | } 1882 | ``` 1883 | 1884 | ### Avoiding Stale Closures 1885 | 1886 | ```tsx 1887 | function Component({ callback }: Props) { 1888 | const callbackRef = useRef(callback); 1889 | 1890 | // Keep ref updated with latest callback 1891 | useEffect(() => { 1892 | callbackRef.current = callback; 1893 | }, [callback]); 1894 | 1895 | useEffect(() => { 1896 | const interval = setInterval(() => { 1897 | // Always uses latest callback 1898 | callbackRef.current(); 1899 | }, 1000); 1900 | 1901 | return () => clearInterval(interval); 1902 | }, []); // No callback dependency needed 1903 | 1904 | return <div>Running...</div>; 1905 | } 1906 | ``` 1907 | 1908 | ### Common Patterns in XMLUI 1909 | 1910 | **Previous Value Tracking:** 1911 | ```tsx 1912 | function usePrevious<T>(value: T): T | undefined { 1913 | const ref = useRef<T>(); 1914 | 1915 | useEffect(() => { 1916 | ref.current = value; 1917 | }, [value]); 1918 | 1919 | return ref.current; 1920 | } 1921 | 1922 | function Component({ count }: Props) { 1923 | const prevCount = usePrevious(count); 1924 | 1925 | return <div>Now: {count}, Before: {prevCount}</div>; 1926 | } 1927 | ``` 1928 | 1929 | ### Key Differences: useState vs useRef 1930 | 1931 | | Feature | `useState` | `useRef` | 1932 | |---------|-----------|---------| 1933 | | Triggers re-render | ✅ Yes | ❌ No | 1934 | | Persists across renders | ✅ Yes | ✅ Yes | 1935 | | Use for UI state | ✅ Yes | ❌ No | 1936 | | Use for DOM access | ❌ No | ✅ Yes | 1937 | | Use for mutable timers/intervals | ❌ No | ✅ Yes | 1938 | 1939 | --- 1940 | 1941 | ## `useId` - Unique ID Generation 1942 | 1943 | **Purpose:** Generate stable unique IDs for accessibility attributes. 1944 | 1945 | **Syntax:** `const id = useId()` 1946 | 1947 | ### Basic Usage 1948 | 1949 | ```tsx 1950 | function FormField({ label }: Props) { 1951 | const id = useId(); 1952 | 1953 | return ( 1954 | <div> 1955 | <label htmlFor={id}>{label}</label> 1956 | <input id={id} /> 1957 | </div> 1958 | ); 1959 | } 1960 | ``` 1961 | 1962 | ### Multiple IDs 1963 | 1964 | ```tsx 1965 | function ComplexForm() { 1966 | const id = useId(); 1967 | 1968 | return ( 1969 | <div> 1970 | <label htmlFor={`${id}-name`}>Name</label> 1971 | <input id={`${id}-name`} aria-describedby={`${id}-name-hint`} /> 1972 | <span id={`${id}-name-hint`}>Enter your full name</span> 1973 | 1974 | <label htmlFor={`${id}-email`}>Email</label> 1975 | <input id={`${id}-email`} /> 1976 | </div> 1977 | ); 1978 | } 1979 | ``` 1980 | 1981 | **Why not just use a counter?** `useId` generates IDs that are stable across server and client rendering, preventing hydration mismatches. 1982 | 1983 | --- 1984 | 1985 | ## `forwardRef` - Ref Forwarding to Child Components 1986 | 1987 | **Purpose:** Allow parent components to access DOM nodes or component instances of child components by forwarding refs through component boundaries. 1988 | 1989 | **Syntax:** `const Component = forwardRef((props, ref) => { ... })` 1990 | 1991 | ### Basic Usage 1992 | 1993 | ```tsx 1994 | const TextInput = forwardRef<HTMLInputElement, Props>( 1995 | function TextInput({ label, ...props }, forwardedRef) { 1996 | return ( 1997 | <div> 1998 | <label>{label}</label> 1999 | <input ref={forwardedRef} {...props} /> 2000 | </div> 2001 | ); 2002 | } 2003 | ); 2004 | 2005 | // Parent can now access the input element 2006 | function Form() { 2007 | const inputRef = useRef<HTMLInputElement>(null); 2008 | 2009 | const focusInput = () => { 2010 | inputRef.current?.focus(); 2011 | }; 2012 | 2013 | return ( 2014 | <div> 2015 | <TextInput ref={inputRef} label="Name" /> 2016 | <button onClick={focusInput}>Focus Input</button> 2017 | </div> 2018 | ); 2019 | } 2020 | ``` 2021 | 2022 | ### TypeScript Generic Syntax 2023 | 2024 | ```tsx 2025 | // Explicitly type both the ref and props 2026 | const Component = forwardRef<RefType, PropsType>( 2027 | function Component(props, ref) { 2028 | return <div ref={ref}>...</div>; 2029 | } 2030 | ); 2031 | 2032 | // Example with HTMLDivElement 2033 | const Card = forwardRef<HTMLDivElement, CardProps>( 2034 | function Card({ children, className }, ref) { 2035 | return ( 2036 | <div ref={ref} className={className}> 2037 | {children} 2038 | </div> 2039 | ); 2040 | } 2041 | ); 2042 | ``` 2043 | 2044 | **Why explicit typing matters:** 2045 | 2046 | Without generic syntax, TypeScript infers types from the function signature, which can lead to several issues: 2047 | 2048 | ```tsx 2049 | // ❌ WRONG - Without explicit generics 2050 | const Input = forwardRef(function Input(props: Props, ref) { 2051 | // TypeScript infers ref as: ForwardedRef<unknown> 2052 | // This means: 2053 | // 1. No autocomplete for ref.current properties 2054 | // 2. No type checking when assigning ref to JSX elements 2055 | // 3. Parent components can pass wrong ref type without errors 2056 | return <input ref={ref} />; // Type error: ref might not be compatible! 2057 | }); 2058 | 2059 | // Usage - TypeScript won't catch this error: 2060 | const divRef = useRef<HTMLDivElement>(null); 2061 | <Input ref={divRef} /> // Should error but doesn't - expecting HTMLInputElement! 2062 | 2063 | // ✅ CORRECT - With explicit generics 2064 | const Input = forwardRef<HTMLInputElement, Props>( 2065 | function Input(props, ref) { 2066 | // TypeScript knows ref is: ForwardedRef<HTMLInputElement> 2067 | // Benefits: 2068 | // 1. Autocomplete works: ref.current?.focus() 2069 | // 2. Type checking ensures ref matches JSX element 2070 | // 3. Parent must pass correct ref type 2071 | return <input ref={ref} />; // Type safe! 2072 | } 2073 | ); 2074 | 2075 | // Usage - TypeScript catches the error: 2076 | const divRef = useRef<HTMLDivElement>(null); 2077 | <Input ref={divRef} /> // ❌ Type error: expected RefObject<HTMLInputElement> 2078 | ``` 2079 | 2080 | **Key problems without explicit generics:** 2081 | 1. **Loss of type safety** - Parent can pass incompatible ref types 2082 | 2. **No IntelliSense** - No autocomplete for `ref.current` properties 2083 | 3. **Runtime errors** - Type mismatches only discovered at runtime 2084 | 4. **Harder refactoring** - Changes to ref type don't propagate to consumers 2085 | 2086 | **Best practice:** Always specify both generic parameters explicitly in XMLUI components. 2087 | 2088 | ### Composing Multiple Refs 2089 | 2090 | **The Problem:** Components often need to manage multiple refs pointing to the same DOM element: 2091 | 1. **Internal ref** - Component's own logic (measurements, animations, focus) 2092 | 2. **Forwarded ref** - Parent needs access to the DOM element 2093 | 3. **Third-party refs** - Integration with libraries (Popper, Radix UI, etc.) 2094 | 2095 | **The Solution:** Use `composeRefs` from `@radix-ui/react-compose-refs` to merge multiple refs into one. 2096 | 2097 | **Why you need to compose refs:** 2098 | 2099 | | Use Case | Example | Reason | 2100 | |----------|---------|--------| 2101 | | **Internal logic + parent access** | Auto-resize textarea | Component measures scrollHeight, parent needs focus() | 2102 | | **Library integration** | Popover/Tooltip | Popper needs ref for positioning, parent needs ref for control | 2103 | | **Wrapper components** | Container with single child | Parent ref applies to child, child has own ref | 2104 | | **Multiple behaviors** | Draggable element | Drag library needs ref, resize observer needs ref, parent needs ref | 2105 | 2106 | **Key differences: Inner vs Forwarded refs:** 2107 | 2108 | | Aspect | Inner Ref | Forwarded Ref | 2109 | |--------|-----------|---------------| 2110 | | **Created by** | Component itself with `useRef()` | Parent component | 2111 | | **Purpose** | Internal component logic | Parent needs DOM access | 2112 | | **Type** | Always `RefObject<T>` | Can be `RefObject<T>`, `RefCallback<T>`, or `null` | 2113 | | **Guaranteed to exist** | Yes - always has `.current` property | No - parent might not pass a ref | 2114 | | **When to use** | Component needs DOM access for its own behavior | Expose DOM element to parent | 2115 | 2116 | **Example 1: Internal + Forwarded (most common in XMLUI):** 2117 | 2118 | ```tsx 2119 | import { composeRefs } from "@radix-ui/react-compose-refs"; 2120 | 2121 | function TextArea({ value, onChange }: Props, forwardedRef: Ref<HTMLTextAreaElement>) { 2122 | // Inner ref: Component creates and owns this for auto-resize logic 2123 | const innerRef = useRef<HTMLTextAreaElement>(null); 2124 | 2125 | // Compose both refs - textarea element needs both 2126 | const composedRef = forwardedRef 2127 | ? composeRefs(innerRef, forwardedRef) 2128 | : innerRef; 2129 | 2130 | useEffect(() => { 2131 | // ✅ CORRECT: Use innerRef for internal logic 2132 | // It's guaranteed to exist and have .current property 2133 | if (innerRef.current) { 2134 | innerRef.current.style.height = 'auto'; 2135 | innerRef.current.style.height = `${innerRef.current.scrollHeight}px`; 2136 | } 2137 | 2138 | // ❌ WRONG: Don't use forwardedRef directly 2139 | // if (forwardedRef?.current) { ... } // Type error: Ref<T> might be a callback! 2140 | }, [value]); 2141 | 2142 | return <textarea ref={composedRef} value={value} onChange={onChange} />; 2143 | } 2144 | 2145 | export const AutoResizeTextArea = forwardRef(TextArea); 2146 | 2147 | // Parent usage: 2148 | function Form() { 2149 | const textareaRef = useRef<HTMLTextAreaElement>(null); 2150 | 2151 | const focusTextarea = () => { 2152 | textareaRef.current?.focus(); // Parent can access via forwarded ref 2153 | }; 2154 | 2155 | return <AutoResizeTextArea ref={textareaRef} />; // Component auto-resizes via inner ref 2156 | } 2157 | ``` 2158 | 2159 | **Example 2: Library Integration (Popper + Forwarded):** 2160 | 2161 | ```tsx 2162 | function Select({ options }: Props, forwardedRef: Ref<HTMLButtonElement>) { 2163 | // Popper library needs a ref for positioning 2164 | const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null); 2165 | 2166 | // Compose library ref setter with forwarded ref 2167 | const composedRef = forwardedRef 2168 | ? composeRefs(setReferenceElement, forwardedRef) 2169 | : setReferenceElement; 2170 | 2171 | return ( 2172 | <> 2173 | <button ref={composedRef}>Select</button> 2174 | <Popper referenceElement={referenceElement}> 2175 | {/* Dropdown content */} 2176 | </Popper> 2177 | </> 2178 | ); 2179 | } 2180 | 2181 | export const SelectComponent = forwardRef(Select); 2182 | ``` 2183 | 2184 | **Example 3: Wrapper Component (Parent + Child refs):** 2185 | 2186 | ```tsx 2187 | function Container({ children }: Props, ref: Ref<HTMLElement>) { 2188 | const renderedChild = renderChild(children); 2189 | 2190 | // If single child, compose parent's ref with child's existing ref 2191 | if (isValidElement(renderedChild)) { 2192 | return cloneElement(renderedChild, { 2193 | ref: composeRefs(ref, (renderedChild as any).ref), 2194 | }); 2195 | } 2196 | 2197 | return renderedChild; 2198 | } 2199 | 2200 | export const ContainerComponent = forwardRef(Container); 2201 | ``` 2202 | 2203 | **Example 4: Multiple Behaviors (Drag + Resize + Forward):** 2204 | 2205 | ```tsx 2206 | function DraggablePanel(props: Props, forwardedRef: Ref<HTMLDivElement>) { 2207 | const dragRef = useRef<HTMLDivElement>(null); 2208 | const resizeObserverRef = useRef<HTMLDivElement>(null); 2209 | 2210 | // Compose all three refs 2211 | const composedRef = composeRefs( 2212 | dragRef, 2213 | resizeObserverRef, 2214 | forwardedRef || null 2215 | ); 2216 | 2217 | useDragLogic(dragRef); 2218 | useResizeObserver(resizeObserverRef); 2219 | 2220 | return <div ref={composedRef}>Draggable and resizable</div>; 2221 | } 2222 | ``` 2223 | 2224 | **When to compose refs:** 2225 | - Component needs internal ref AND parent needs access 2226 | - Integrating with libraries that require refs (Popper, React DnD, etc.) 2227 | - Wrapping components that need to forward refs to children 2228 | - Multiple hooks/effects need refs to the same element 2229 | 2230 | **How `composeRefs` works:** 2231 | - Accepts multiple refs (RefObjects, callbacks, or null) 2232 | - Returns a single callback ref that updates all provided refs 2233 | - Handles both RefObject (sets `.current`) and callback refs (calls function) 2234 | - Safely ignores `null`/`undefined` refs 2235 | 2236 | ### When to Use forwardRef 2237 | 2238 | **Use `forwardRef` when:** 2239 | - Building reusable components that wrap DOM elements 2240 | - Parent needs direct DOM access (focus, scroll, measurements) 2241 | - Integrating with third-party libraries requiring refs 2242 | - Creating form components that need imperative control 2243 | 2244 | **Don't use `forwardRef` when:** 2245 | - Component doesn't wrap a single DOM element 2246 | - Refs aren't needed by parent components 2247 | - You can solve the problem with callbacks/props instead 2248 | 2249 | ### Common Mistakes 2250 | 2251 | ```tsx 2252 | // ❌ WRONG - Forgetting to attach ref to DOM element 2253 | const Bad = forwardRef((props, ref) => { 2254 | return <div>{props.children}</div>; // ref is ignored! 2255 | }); 2256 | 2257 | // ✅ CORRECT - Always attach ref to actual DOM element 2258 | const Good = forwardRef((props, ref) => { 2259 | return <div ref={ref}>{props.children}</div>; 2260 | }); 2261 | 2262 | // ❌ WRONG - Attaching ref to component (won't work) 2263 | const AlsoBad = forwardRef((props, ref) => { 2264 | return <CustomComponent ref={ref} />; // CustomComponent must also use forwardRef 2265 | }); 2266 | 2267 | // ✅ CORRECT - Forward through nested components 2268 | const CustomComponent = forwardRef((props, ref) => { 2269 | return <div ref={ref}>...</div>; 2270 | }); 2271 | 2272 | const AlsoGood = forwardRef((props, ref) => { 2273 | return <CustomComponent ref={ref} />; // Works because CustomComponent forwards 2274 | }); 2275 | ``` 2276 | 2277 | --- 2278 | 2279 | ## `createPortal` - Render Outside Hierarchy 2280 | 2281 | **Purpose:** Render children into a DOM node outside the parent component's hierarchy. 2282 | 2283 | **Syntax:** `createPortal(children, domNode, key?)` 2284 | 2285 | ### Basic Usage 2286 | 2287 | ```tsx 2288 | import { createPortal } from 'react-dom'; 2289 | 2290 | function Modal({ isOpen, children }: Props) { 2291 | if (!isOpen) return null; 2292 | 2293 | // Render into document.body instead of parent component 2294 | return createPortal( 2295 | <div className="modal-overlay"> 2296 | <div className="modal-content"> 2297 | {children} 2298 | </div> 2299 | </div>, 2300 | document.body 2301 | ); 2302 | } 2303 | 2304 | // Usage 2305 | function App() { 2306 | return ( 2307 | <div className="app"> 2308 | <Modal isOpen={true}> 2309 | <h1>This renders in document.body, not .app!</h1> 2310 | </Modal> 2311 | </div> 2312 | ); 2313 | } 2314 | ``` 2315 | 2316 | ### Common Use Cases 2317 | 2318 | **1. Tooltips/Popovers (avoid z-index issues):** 2319 | ```tsx 2320 | function Tooltip({ targetRef, content }: Props) { 2321 | return createPortal( 2322 | <div className="tooltip" style={calculatePosition(targetRef)}> 2323 | {content} 2324 | </div>, 2325 | document.body 2326 | ); 2327 | } 2328 | ``` 2329 | 2330 | **2. Notifications/Toasts:** 2331 | ```tsx 2332 | function NotificationToast() { 2333 | const [shouldRender, setShouldRender] = useState(false); 2334 | 2335 | useEffect(() => { 2336 | setShouldRender(true); 2337 | }, []); 2338 | 2339 | if (!shouldRender) return null; 2340 | 2341 | return createPortal( 2342 | <Toaster position="top-right"> 2343 | {(t) => <ToastBar toast={t} />} 2344 | </Toaster>, 2345 | document.body 2346 | ); 2347 | } 2348 | ``` 2349 | 2350 | **3. Modal Dialogs:** 2351 | ```tsx 2352 | function ModalDialog({ isOpen, children }: Props) { 2353 | if (!isOpen) return null; 2354 | 2355 | return createPortal( 2356 | <div className="modal-backdrop"> 2357 | <div className="modal-dialog"> 2358 | {children} 2359 | </div> 2360 | </div>, 2361 | document.getElementById('modal-root') || document.body 2362 | ); 2363 | } 2364 | ``` 2365 | 2366 | **4. Full-Screen Overlays:** 2367 | ```tsx 2368 | function FullScreenOverlay({ show, children }: Props) { 2369 | if (!show) return null; 2370 | 2371 | return createPortal( 2372 | <div className="fullscreen-overlay"> 2373 | {children} 2374 | </div>, 2375 | document.body 2376 | ); 2377 | } 2378 | ``` 2379 | 2380 | ### Event Bubbling Still Works 2381 | 2382 | ```tsx 2383 | // Event bubbling works despite DOM hierarchy 2384 | function Parent() { 2385 | const handleClick = () => { 2386 | console.log('Clicked!'); // This fires even though button is portaled 2387 | }; 2388 | 2389 | return ( 2390 | <div onClick={handleClick}> 2391 | <PortaledButton /> 2392 | </div> 2393 | ); 2394 | } 2395 | 2396 | function PortaledButton() { 2397 | return createPortal( 2398 | <button>Click me</button>, 2399 | document.body 2400 | ); 2401 | } 2402 | ``` 2403 | 2404 | ### Common Pattern in XMLUI 2405 | 2406 | ```tsx 2407 | // App component portals theme styles 2408 | function App({ children }: Props) { 2409 | return ( 2410 | <> 2411 | {children} 2412 | {createPortal( 2413 | <style>{themeCSS}</style>, 2414 | document.head 2415 | )} 2416 | </> 2417 | ); 2418 | } 2419 | 2420 | // Inspector portals debugging UI 2421 | function Inspector() { 2422 | return createPortal( 2423 | <div className="inspector-panel"> 2424 | {/* Debug tools */} 2425 | </div>, 2426 | document.body 2427 | ); 2428 | } 2429 | ``` 2430 | 2431 | ### When to Use createPortal 2432 | 2433 | **Use `createPortal` when:** 2434 | - Modals, dialogs, and overlays 2435 | - Tooltips and popovers 2436 | - Notifications and toasts 2437 | - Avoiding parent overflow/z-index issues 2438 | - Rendering into different parts of DOM (head, body) 2439 | 2440 | **Don't use when:** 2441 | - Normal component rendering is sufficient 2442 | - No CSS stacking or overflow issues 2443 | - Adds unnecessary complexity 2444 | 2445 | --- 2446 | 2447 | ## `Fragment` - Grouping Without DOM Nodes 2448 | 2449 | **Purpose:** Group multiple elements without adding extra nodes to the DOM. 2450 | 2451 | **Syntax:** `<Fragment>...</Fragment>` or `<>...</>` 2452 | 2453 | ### Basic Usage 2454 | 2455 | ```tsx 2456 | // ❌ WRONG - Adds unnecessary div wrapper 2457 | function List() { 2458 | return ( 2459 | <div> 2460 | <li>Item 1</li> 2461 | <li>Item 2</li> 2462 | </div> 2463 | ); 2464 | } 2465 | 2466 | // ✅ CORRECT - No extra DOM node 2467 | function List() { 2468 | return ( 2469 | <> 2470 | <li>Item 1</li> 2471 | <li>Item 2</li> 2472 | </> 2473 | ); 2474 | } 2475 | ``` 2476 | 2477 | ### Short vs Long Syntax 2478 | 2479 | ```tsx 2480 | // Short syntax <> - Use for most cases 2481 | function Component() { 2482 | return ( 2483 | <> 2484 | <Header /> 2485 | <Content /> 2486 | </> 2487 | ); 2488 | } 2489 | 2490 | // Long syntax <Fragment> - Required when you need a key 2491 | function List({ items }: Props) { 2492 | return ( 2493 | <ul> 2494 | {items.map(item => ( 2495 | <Fragment key={item.id}> 2496 | <li>{item.name}</li> 2497 | <li>{item.description}</li> 2498 | </Fragment> 2499 | ))} 2500 | </ul> 2501 | ); 2502 | } 2503 | ``` 2504 | 2505 | **Limitations of short syntax:** 2506 | - ❌ Cannot add `key` prop (use `<Fragment key={...}>` instead) 2507 | - ❌ Cannot add any other props (only `key` is allowed on Fragment) 2508 | - ✅ Use short syntax everywhere else (cleaner, less verbose) 2509 | 2510 | ### Common Use Cases 2511 | 2512 | **1. Returning Multiple Elements:** 2513 | ```tsx 2514 | function Header() { 2515 | return ( 2516 | <> 2517 | <h1>Title</h1> 2518 | <nav>Navigation</nav> 2519 | </> 2520 | ); 2521 | } 2522 | ``` 2523 | 2524 | **2. Conditional Rendering:** 2525 | ```tsx 2526 | function Component({ showExtra }: Props) { 2527 | return ( 2528 | <div> 2529 | <h1>Always shown</h1> 2530 | {showExtra && ( 2531 | <> 2532 | <p>Extra content</p> 2533 | <button>Extra button</button> 2534 | </> 2535 | )} 2536 | </div> 2537 | ); 2538 | } 2539 | ``` 2540 | 2541 | **3. Table Rows:** 2542 | ```tsx 2543 | function TableRows({ data }: Props) { 2544 | return ( 2545 | <> 2546 | {data.map(row => ( 2547 | <Fragment key={row.id}> 2548 | <tr> 2549 | <td>{row.name}</td> 2550 | <td>{row.value}</td> 2551 | </tr> 2552 | {row.hasDetails && ( 2553 | <tr> 2554 | <td colSpan={2}>{row.details}</td> 2555 | </tr> 2556 | )} 2557 | </Fragment> 2558 | ))} 2559 | </> 2560 | ); 2561 | } 2562 | ``` 2563 | 2564 | **4. Avoiding Invalid HTML:** 2565 | ```tsx 2566 | // ❌ WRONG - div inside p is invalid HTML 2567 | function Text() { 2568 | return ( 2569 | <p> 2570 | <div>This is invalid!</div> 2571 | </p> 2572 | ); 2573 | } 2574 | 2575 | // ✅ CORRECT - Fragment doesn't create DOM node 2576 | function Text() { 2577 | return ( 2578 | <p> 2579 | <> 2580 | <span>This is valid!</span> 2581 | </> 2582 | </p> 2583 | ); 2584 | } 2585 | ``` 2586 | 2587 | ### When to Use Fragment 2588 | 2589 | **Use `Fragment` when:** 2590 | - Component must return multiple elements 2591 | - Avoiding wrapper divs that break CSS (flexbox, grid) 2592 | - Keeping HTML semantically valid 2593 | - Conditional rendering of multiple elements 2594 | 2595 | **Don't use when:** 2596 | - Single element (no need to wrap) 2597 | - Wrapper div doesn't cause issues 2598 | - Need to attach events or refs (Fragment can't have them) 2599 | 2600 | --- 2601 | 2602 | ## `cloneElement` - Clone and Modify React Elements 2603 | 2604 | **Purpose:** Clone a React element and override its props, refs, or children. 2605 | 2606 | **Syntax:** `cloneElement(element, props?, ...children?)` 2607 | 2608 | ### Basic Usage 2609 | 2610 | ```tsx 2611 | import { cloneElement, isValidElement } from 'react'; 2612 | 2613 | function Container({ children }: Props) { 2614 | if (!isValidElement(children)) { 2615 | return children; 2616 | } 2617 | 2618 | // Clone child and add extra props 2619 | return cloneElement(children, { 2620 | className: 'container-child', 2621 | style: { padding: '10px' }, 2622 | }); 2623 | } 2624 | 2625 | // Usage 2626 | <Container> 2627 | <div>Original</div> {/* Becomes <div className="container-child" style={{padding: '10px'}}>Original</div> */} 2628 | </Container> 2629 | ``` 2630 | 2631 | ### Adding Props to Children 2632 | 2633 | ```tsx 2634 | function Animation({ children, duration = 300 }: Props) { 2635 | if (!isValidElement(children)) { 2636 | return children; 2637 | } 2638 | 2639 | // Add animation props to child 2640 | return cloneElement(children, { 2641 | style: { 2642 | ...children.props.style, 2643 | transition: `all ${duration}ms`, 2644 | }, 2645 | }); 2646 | } 2647 | ``` 2648 | 2649 | ### Forwarding Refs Through Clone 2650 | 2651 | ```tsx 2652 | function Wrapper({ children, ...rest }: Props, forwardedRef: Ref<any>) { 2653 | if (!isValidElement(children)) { 2654 | return children; 2655 | } 2656 | 2657 | // Clone and forward ref + other props 2658 | return cloneElement(children, { 2659 | ...rest, 2660 | ref: forwardedRef, 2661 | }); 2662 | } 2663 | 2664 | export const WrapperComponent = forwardRef(Wrapper); 2665 | ``` 2666 | 2667 | ### Common Pattern in XMLUI 2668 | 2669 | **Container with single child ref forwarding:** 2670 | ```tsx 2671 | function Container({ children }: Props, ref: Ref<HTMLElement>) { 2672 | const renderedChild = renderChild(children); 2673 | 2674 | // If single valid child, compose refs and merge props 2675 | if (ref && renderedChild && isValidElement(renderedChild)) { 2676 | return cloneElement(renderedChild, { 2677 | ref: composeRefs(ref, (renderedChild as any).ref), 2678 | ...mergeProps(renderedChild.props, rest), 2679 | }); 2680 | } 2681 | 2682 | return renderedChild; 2683 | } 2684 | ``` 2685 | 2686 | **Form field with label integration:** 2687 | ```tsx 2688 | function ItemWithLabel({ children, label }: Props) { 2689 | const id = useId(); 2690 | 2691 | return ( 2692 | <div> 2693 | <label htmlFor={id}>{label}</label> 2694 | {cloneElement(children as ReactElement, { 2695 | id, 2696 | 'aria-labelledby': id, 2697 | })} 2698 | </div> 2699 | ); 2700 | } 2701 | ``` 2702 | 2703 | ### When to Use cloneElement 2704 | 2705 | **Use `cloneElement` when:** 2706 | - Wrapping components need to add props to children 2707 | - Forwarding refs through wrapper components 2708 | - Adding common behavior to arbitrary children 2709 | - Integrating with child elements you don't control 2710 | 2711 | **Don't use when:** 2712 | - You can pass props directly (prefer explicit props) 2713 | - You need to modify deeply nested children (use context instead) 2714 | - Children are not React elements (check with `isValidElement` first) 2715 | 2716 | ### Common Mistakes 2717 | 2718 | ```tsx 2719 | // ❌ WRONG - Not checking if child is valid element 2720 | function Bad({ children }: Props) { 2721 | return cloneElement(children, { className: 'bad' }); // Crashes if children is string/number 2722 | } 2723 | 2724 | // ✅ CORRECT - Always validate first (see isValidElement section) 2725 | function Good({ children }: Props) { 2726 | if (!isValidElement(children)) { 2727 | return children; 2728 | } 2729 | return cloneElement(children, { className: 'good' }); 2730 | } 2731 | 2732 | // ❌ WRONG - Overriding all existing props 2733 | return cloneElement(child, { style: { color: 'red' } }); // Loses child's existing style 2734 | 2735 | // ✅ CORRECT - Merge with existing props 2736 | return cloneElement(child, { 2737 | style: { ...child.props.style, color: 'red' }, 2738 | }); 2739 | ``` 2740 | 2741 | --- 2742 | 2743 | ## `isValidElement` - Type Check for React Elements 2744 | 2745 | **Purpose:** Check if a value is a valid React element (created with JSX or `createElement`). Always use before `cloneElement`. 2746 | 2747 | **Syntax:** `isValidElement(value)` 2748 | 2749 | ### Basic Usage 2750 | 2751 | ```tsx 2752 | import { isValidElement } from 'react'; 2753 | 2754 | function processChild(child: React.ReactNode) { 2755 | // child could be anything: string, number, element, array, etc. 2756 | 2757 | if (isValidElement(child)) { 2758 | // TypeScript now knows child is ReactElement 2759 | console.log(child.props); // ✅ OK - access props safely 2760 | console.log(child.type); // ✅ OK - access type safely 2761 | return child; 2762 | } 2763 | 2764 | // Not an element - return as is 2765 | return child; 2766 | } 2767 | ``` 2768 | 2769 | ### Common Pattern in XMLUI 2770 | 2771 | **Conditional element wrapping:** 2772 | ```tsx 2773 | function ConditionalWrapper({ condition, children }: Props) { 2774 | if (!condition) { 2775 | return children; 2776 | } 2777 | 2778 | // Only wrap if child is valid element 2779 | return isValidElement(children) 2780 | ? <div className="wrapper">{children}</div> 2781 | : children; 2782 | } 2783 | ``` 2784 | 2785 | ### What isValidElement Checks 2786 | 2787 | ```tsx 2788 | isValidElement(<div />); // ✅ true - JSX element 2789 | isValidElement(React.createElement('div')); // ✅ true - created element 2790 | isValidElement(<Component />); // ✅ true - component element 2791 | isValidElement('hello'); // ❌ false - string 2792 | isValidElement(123); // ❌ false - number 2793 | isValidElement(null); // ❌ false - null 2794 | isValidElement(undefined); // ❌ false - undefined 2795 | isValidElement([<div key="1" />]); // ❌ false - array of elements 2796 | ``` 2797 | 2798 | ### When to Use isValidElement 2799 | 2800 | **Use `isValidElement` when:** 2801 | - Before calling `cloneElement` (required to avoid crashes) 2802 | - Type narrowing for TypeScript (ReactNode → ReactElement) 2803 | - Validating `children` prop type 2804 | - Conditional element manipulation 2805 | 2806 | **Note:** See `cloneElement` section for examples of using these two functions together. 2807 | 2808 | --- 2809 | 2810 | ## `flushSync` - Synchronous State Updates 2811 | 2812 | **Purpose:** Force React to flush state updates synchronously, bypassing automatic batching. 2813 | 2814 | **Syntax:** `flushSync(() => { /* state updates */ })` 2815 | 2816 | **Warning:** Use sparingly - breaks React's batching optimization and can hurt performance. 2817 | 2818 | ### Basic Usage 2819 | 2820 | ```tsx 2821 | import { flushSync } from 'react-dom'; 2822 | 2823 | function Form() { 2824 | const [value, setValue] = useState(''); 2825 | 2826 | const handleSubmit = () => { 2827 | // Normal: state updates are batched 2828 | setValue(''); 2829 | setError(null); 2830 | // Both updates happen together 2831 | 2832 | // With flushSync: update happens immediately 2833 | flushSync(() => { 2834 | setValue(''); 2835 | }); 2836 | // DOM is updated here, before next line 2837 | inputRef.current?.focus(); 2838 | }; 2839 | } 2840 | ``` 2841 | 2842 | ### When DOM Must Update Immediately 2843 | 2844 | ```tsx 2845 | function Table({ data }: Props) { 2846 | const [selectedRow, setSelectedRow] = useState(0); 2847 | const rowRef = useRef<HTMLTableRowElement>(null); 2848 | 2849 | const selectRow = (index: number) => { 2850 | // Must update DOM before scrolling 2851 | flushSync(() => { 2852 | setSelectedRow(index); 2853 | }); 2854 | 2855 | // DOM is updated, can now scroll 2856 | rowRef.current?.scrollIntoView(); 2857 | }; 2858 | } 2859 | ``` 2860 | 2861 | ### Common Pattern in XMLUI 2862 | 2863 | **Form reset with focus:** 2864 | ```tsx 2865 | function Form({ onSubmit }: Props) { 2866 | const doReset = () => { 2867 | // Reset all fields 2868 | }; 2869 | 2870 | const handleSuccess = () => { 2871 | const prevFocused = document.activeElement; 2872 | 2873 | // Force synchronous reset before restoring focus 2874 | flushSync(() => { 2875 | doReset(); 2876 | }); 2877 | 2878 | // DOM is reset, restore focus 2879 | if (prevFocused && typeof (prevFocused as HTMLElement).focus === 'function') { 2880 | (prevFocused as HTMLElement).focus(); 2881 | } 2882 | }; 2883 | } 2884 | ``` 2885 | 2886 | **Table with immediate scroll:** 2887 | ```tsx 2888 | function DataTable({ data }: Props) { 2889 | const handleSort = (column: string) => { 2890 | // Update sort synchronously before scrolling 2891 | flushSync(() => { 2892 | setSortColumn(column); 2893 | setSortedData(sortData(data, column)); 2894 | }); 2895 | 2896 | // Table is re-rendered, can scroll to top 2897 | tableRef.current?.scrollTo(0, 0); 2898 | }; 2899 | } 2900 | ``` 2901 | 2902 | ### Why flushSync Exists 2903 | 2904 | ```tsx 2905 | // ❌ Problem: Without flushSync 2906 | function Component() { 2907 | const [text, setText] = useState(''); 2908 | 2909 | const update = () => { 2910 | setText('new value'); 2911 | // DOM not updated yet! 2912 | inputRef.current?.focus(); // Focuses old state 2913 | }; 2914 | } 2915 | 2916 | // ✅ Solution: With flushSync 2917 | function Component() { 2918 | const [text, setText] = useState(''); 2919 | 2920 | const update = () => { 2921 | flushSync(() => { 2922 | setText('new value'); 2923 | }); 2924 | // DOM is updated 2925 | inputRef.current?.focus(); // Focuses new state 2926 | }; 2927 | } 2928 | ``` 2929 | 2930 | ### When to Use flushSync 2931 | 2932 | **Use `flushSync` when:** 2933 | - Need DOM measurements after state change 2934 | - Synchronizing with third-party libraries 2935 | - Scrolling after state update 2936 | - Focus management after state change 2937 | 2938 | **Don't use when:** 2939 | - Normal state updates (let React batch) 2940 | - Performance-critical code paths 2941 | - You can solve it with `useLayoutEffect` 2942 | - Inside render (not allowed) 2943 | 2944 | **Performance impact:** 2945 | ```tsx 2946 | // ❌ BAD - Multiple flushSync calls 2947 | data.forEach(item => { 2948 | flushSync(() => { 2949 | processItem(item); // Forces re-render each time 2950 | }); 2951 | }); 2952 | 2953 | // ✅ GOOD - Single batch update 2954 | const processedItems = data.map(processItem); 2955 | flushSync(() => { 2956 | setItems(processedItems); // Single re-render 2957 | }); 2958 | ``` 2959 | 2960 | --- 2961 | 2962 | ## `createRoot` - React 18 Root API 2963 | 2964 | **Purpose:** Create a root to render React components into a DOM container (React 18+). 2965 | 2966 | **Syntax:** `const root = createRoot(container); root.render(<App />)` 2967 | 2968 | ### Basic Usage 2969 | 2970 | ```tsx 2971 | import { createRoot } from 'react-dom/client'; 2972 | 2973 | // Old way (React 17) 2974 | ReactDOM.render(<App />, document.getElementById('root')); 2975 | 2976 | // New way (React 18+) 2977 | const root = createRoot(document.getElementById('root')!); 2978 | root.render(<App />); 2979 | ``` 2980 | 2981 | ### With TypeScript 2982 | 2983 | ```tsx 2984 | import { createRoot } from 'react-dom/client'; 2985 | 2986 | const container = document.getElementById('root'); 2987 | if (!container) { 2988 | throw new Error('Root element not found'); 2989 | } 2990 | 2991 | const root = createRoot(container); 2992 | root.render(<App />); 2993 | ``` 2994 | 2995 | ### Unmounting 2996 | 2997 | ```tsx 2998 | const root = createRoot(container); 2999 | root.render(<App />); 3000 | 3001 | // Later: unmount 3002 | root.unmount(); 3003 | ``` 3004 | 3005 | ### Common Pattern in XMLUI 3006 | 3007 | **Standalone app rendering:** 3008 | ```tsx 3009 | function renderStandaloneApp(rootElement: HTMLElement) { 3010 | let contentRoot: Root; 3011 | 3012 | if (!contentRoot) { 3013 | contentRoot = createRoot(rootElement); 3014 | } 3015 | 3016 | contentRoot.render( 3017 | <StrictMode> 3018 | <App /> 3019 | </StrictMode> 3020 | ); 3021 | 3022 | return contentRoot; 3023 | } 3024 | ``` 3025 | 3026 | **Shadow DOM rendering:** 3027 | ```tsx 3028 | function NestedApp({ children }: Props) { 3029 | const shadowRef = useRef<ShadowRoot>(null); 3030 | const contentRootRef = useRef<Root | null>(null); 3031 | 3032 | useEffect(() => { 3033 | if (shadowRef.current && !contentRootRef.current) { 3034 | // Create root in shadow DOM 3035 | contentRootRef.current = createRoot(shadowRef.current); 3036 | contentRootRef.current.render(<NestedContent />); 3037 | } 3038 | 3039 | return () => { 3040 | contentRootRef.current?.unmount(); 3041 | }; 3042 | }, []); 3043 | } 3044 | ``` 3045 | 3046 | ### Benefits of createRoot (React 18) 3047 | 3048 | 1. **Automatic batching** - All updates batched, even in promises/setTimeout 3049 | 2. **Concurrent features** - Enables `useTransition`, `useDeferredValue`, etc. 3050 | 3. **Improved hydration** - Better SSR support 3051 | 4. **Suspense improvements** - Better streaming SSR 3052 | 3053 | ### When to Use createRoot 3054 | 3055 | **Use `createRoot` when:** 3056 | - Starting a new React 18+ application 3057 | - Rendering React into a DOM container 3058 | - Creating multiple roots in one app 3059 | - Rendering into shadow DOM 3060 | 3061 | **Migration from React 17:** 3062 | ```tsx 3063 | // React 17 3064 | import ReactDOM from 'react-dom'; 3065 | ReactDOM.render(<App />, container); 3066 | ReactDOM.unmountComponentAtNode(container); 3067 | 3068 | // React 18 3069 | import { createRoot } from 'react-dom/client'; 3070 | const root = createRoot(container); 3071 | root.render(<App />); 3072 | root.unmount(); 3073 | ``` 3074 | 3075 | --- 3076 | 3077 | ## React Accessibility Patterns 3078 | 3079 | ### ARIA Attributes Pattern 3080 | 3081 | **Key ARIA attributes:** `role`, `aria-label`, `aria-labelledby`, `aria-describedby`, `aria-hidden`, `aria-live`, `aria-expanded`, `aria-selected`, `aria-disabled`, `aria-current` 3082 | 3083 | **Common patterns:** 3084 | 3085 | ```tsx 3086 | // Icon button with accessible label 3087 | <button onClick={onClick} aria-label="Close dialog"> 3088 | <Icon name="close" aria-hidden="true" /> 3089 | </button> 3090 | 3091 | // Form field with error/help text 3092 | function FormField({ label, error, helpText }: Props) { 3093 | const id = useId(); 3094 | return ( 3095 | <> 3096 | <label htmlFor={id}>{label}</label> 3097 | <input id={id} aria-describedby={`${id}-desc`} aria-invalid={!!error} /> 3098 | <span id={`${id}-desc`} role={error ? "alert" : undefined}> 3099 | {error || helpText} 3100 | </span> 3101 | </> 3102 | ); 3103 | } 3104 | 3105 | // Accordion/expandable section 3106 | <button aria-expanded={isOpen} aria-controls={contentId}> 3107 | {title} 3108 | </button> 3109 | <div id={contentId} hidden={!isOpen} role="region"> 3110 | {children} 3111 | </div> 3112 | 3113 | // Live region for announcements 3114 | <div role="status" aria-live="polite" aria-atomic="true"> 3115 | {message} 3116 | </div> 3117 | 3118 | // Modal dialog 3119 | <div role="dialog" aria-modal="true" aria-labelledby={titleId}> 3120 | <h2 id={titleId}>{title}</h2> 3121 | {children} 3122 | </div> 3123 | 3124 | // Tab navigation 3125 | <div role="tablist"> 3126 | <button role="tab" aria-selected={isActive} aria-controls={panelId}> 3127 | {label} 3128 | </button> 3129 | </div> 3130 | <div role="tabpanel" id={panelId} aria-labelledby={tabId}> 3131 | {content} 3132 | </div> 3133 | ``` 3134 | 3135 | **Rules:** Use semantic HTML first, add ARIA only when needed, keep attributes in sync with state, test with screen readers. 3136 | 3137 | --- 3138 | 3139 | ### Focus Management Pattern 3140 | 3141 | **Common scenarios:** Auto-focus on mount, focus traps in modals, focus restoration, roving tab index. 3142 | 3143 | ```tsx 3144 | // Auto-focus first element in dialog 3145 | function Dialog({ isOpen }: Props) { 3146 | const buttonRef = useRef<HTMLButtonElement>(null); 3147 | 3148 | useEffect(() => { 3149 | if (isOpen) buttonRef.current?.focus(); 3150 | }, [isOpen]); 3151 | 3152 | return <button ref={buttonRef}>Close</button>; 3153 | } 3154 | 3155 | // Focus trap + restoration in modal 3156 | function Modal({ isOpen, onClose, children }: Props) { 3157 | const modalRef = useRef<HTMLDivElement>(null); 3158 | const restoreFocusRef = useRef<HTMLElement | null>(null); 3159 | 3160 | useEffect(() => { 3161 | if (!isOpen) return; 3162 | 3163 | restoreFocusRef.current = document.activeElement as HTMLElement; 3164 | modalRef.current?.querySelector<HTMLElement>('button')?.focus(); 3165 | 3166 | return () => restoreFocusRef.current?.focus(); 3167 | }, [isOpen]); 3168 | 3169 | const handleKeyDown = (e: React.KeyboardEvent) => { 3170 | if (e.key === 'Escape') onClose(); 3171 | 3172 | // Trap Tab key 3173 | if (e.key === 'Tab') { 3174 | const focusable = modalRef.current?.querySelectorAll<HTMLElement>( 3175 | 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' 3176 | ); 3177 | if (!focusable?.length) return; 3178 | 3179 | const first = focusable[0]; 3180 | const last = focusable[focusable.length - 1]; 3181 | 3182 | if (e.shiftKey && document.activeElement === first) { 3183 | e.preventDefault(); 3184 | last.focus(); 3185 | } else if (!e.shiftKey && document.activeElement === last) { 3186 | e.preventDefault(); 3187 | first.focus(); 3188 | } 3189 | } 3190 | }; 3191 | 3192 | return ( 3193 | <div ref={modalRef} role="dialog" aria-modal="true" onKeyDown={handleKeyDown}> 3194 | {children} 3195 | </div> 3196 | ); 3197 | } 3198 | 3199 | // Focus after delete action 3200 | function DeleteButton({ itemId, onDelete }: Props) { 3201 | const handleDelete = () => { 3202 | const current = document.getElementById(`item-${itemId}`); 3203 | const next = (current?.nextElementSibling || current?.previousElementSibling) 3204 | ?.querySelector('button') as HTMLElement; 3205 | 3206 | onDelete(itemId); 3207 | setTimeout(() => next?.focus(), 0); 3208 | }; 3209 | 3210 | return <button onClick={handleDelete}>Delete</button>; 3211 | } 3212 | 3213 | // Roving tab index for lists 3214 | function RadioGroup({ options, value, onChange }: Props) { 3215 | const [focusedIndex, setFocusedIndex] = useState(0); 3216 | 3217 | const handleKeyDown = (e: React.KeyboardEvent, index: number) => { 3218 | let newIndex = index; 3219 | if (e.key === 'ArrowDown') newIndex = (index + 1) % options.length; 3220 | if (e.key === 'ArrowUp') newIndex = index === 0 ? options.length - 1 : index - 1; 3221 | if (e.key === 'Home') newIndex = 0; 3222 | if (e.key === 'End') newIndex = options.length - 1; 3223 | 3224 | if (newIndex !== index) { 3225 | e.preventDefault(); 3226 | setFocusedIndex(newIndex); 3227 | } 3228 | }; 3229 | 3230 | return ( 3231 | <div role="radiogroup"> 3232 | {options.map((opt, i) => ( 3233 | <div 3234 | key={opt.id} 3235 | role="radio" 3236 | aria-checked={value === opt.id} 3237 | tabIndex={focusedIndex === i ? 0 : -1} 3238 | onClick={() => onChange(opt.id)} 3239 | onKeyDown={(e) => handleKeyDown(e, i)} 3240 | onFocus={() => setFocusedIndex(i)} 3241 | > 3242 | {opt.label} 3243 | </div> 3244 | ))} 3245 | </div> 3246 | ); 3247 | } 3248 | ``` 3249 | 3250 | **Rules:** Always restore focus when closing modals, trap focus within modal contexts, use `focus-visible` for keyboard-only indicators, test thoroughly. 3251 | 3252 | --- 3253 | 3254 | ### Keyboard Navigation Pattern 3255 | 3256 | **Standard keyboard shortcuts:** Escape (close), Tab/Shift+Tab (navigate), Arrow keys (move focus), Enter/Space (activate), Home/End (first/last). 3257 | 3258 | ```tsx 3259 | // Dropdown with full keyboard support 3260 | function Dropdown({ trigger, items, onSelect }: Props) { 3261 | const [isOpen, setIsOpen] = useState(false); 3262 | const [focusedIndex, setFocusedIndex] = useState(0); 3263 | const itemsRef = useRef<(HTMLButtonElement | null)[]>([]); 3264 | 3265 | const handleKeyDown = (e: React.KeyboardEvent) => { 3266 | switch (e.key) { 3267 | case 'ArrowDown': 3268 | e.preventDefault(); 3269 | if (!isOpen) { 3270 | setIsOpen(true); 3271 | } else { 3272 | const next = (focusedIndex + 1) % items.length; 3273 | setFocusedIndex(next); 3274 | itemsRef.current[next]?.focus(); 3275 | } 3276 | break; 3277 | case 'ArrowUp': 3278 | e.preventDefault(); 3279 | if (isOpen) { 3280 | const prev = focusedIndex === 0 ? items.length - 1 : focusedIndex - 1; 3281 | setFocusedIndex(prev); 3282 | itemsRef.current[prev]?.focus(); 3283 | } 3284 | break; 3285 | case 'Escape': 3286 | e.preventDefault(); 3287 | setIsOpen(false); 3288 | break; 3289 | case 'Enter': 3290 | case ' ': 3291 | e.preventDefault(); 3292 | if (isOpen) { 3293 | onSelect(items[focusedIndex]); 3294 | setIsOpen(false); 3295 | } else { 3296 | setIsOpen(true); 3297 | } 3298 | break; 3299 | } 3300 | }; 3301 | 3302 | return ( 3303 | <div onKeyDown={handleKeyDown}> 3304 | <button onClick={() => setIsOpen(!isOpen)} aria-expanded={isOpen}> 3305 | {trigger} 3306 | </button> 3307 | {isOpen && ( 3308 | <ul role="menu"> 3309 | {items.map((item, i) => ( 3310 | <li key={item.id} role="none"> 3311 | <button 3312 | ref={el => itemsRef.current[i] = el} 3313 | role="menuitem" 3314 | onClick={() => { onSelect(item); setIsOpen(false); }} 3315 | onFocus={() => setFocusedIndex(i)} 3316 | > 3317 | {item.label} 3318 | </button> 3319 | </li> 3320 | ))} 3321 | </ul> 3322 | )} 3323 | </div> 3324 | ); 3325 | } 3326 | 3327 | // Global keyboard shortcuts hook 3328 | function useKeyboardShortcuts(shortcuts: Record<string, () => void>) { 3329 | useEffect(() => { 3330 | const handleKeyDown = (e: KeyboardEvent) => { 3331 | const keys: string[] = []; 3332 | if (e.ctrlKey || e.metaKey) keys.push('Ctrl'); 3333 | if (e.shiftKey) keys.push('Shift'); 3334 | if (e.altKey) keys.push('Alt'); 3335 | keys.push(e.key.toUpperCase()); 3336 | 3337 | const handler = shortcuts[keys.join('+')]; 3338 | if (handler) { 3339 | e.preventDefault(); 3340 | handler(); 3341 | } 3342 | }; 3343 | 3344 | document.addEventListener('keydown', handleKeyDown); 3345 | return () => document.removeEventListener('keydown', handleKeyDown); 3346 | }, [shortcuts]); 3347 | } 3348 | 3349 | // Usage: Editor with shortcuts 3350 | function Editor() { 3351 | useKeyboardShortcuts({ 3352 | 'Ctrl+S': handleSave, 3353 | 'Ctrl+Z': handleUndo, 3354 | 'Ctrl+Shift+Z': handleRedo, 3355 | }); 3356 | return <div>Editor</div>; 3357 | } 3358 | 3359 | // Data table with arrow key navigation 3360 | function DataTable({ columns, rows }: Props) { 3361 | const [focusedCell, setFocusedCell] = useState({ row: 0, col: 0 }); 3362 | const cellRefs = useRef<(HTMLTableCellElement | null)[][]>([]); 3363 | 3364 | const handleKeyDown = (e: React.KeyboardEvent, rowIndex: number, colIndex: number) => { 3365 | let newRow = rowIndex, newCol = colIndex; 3366 | 3367 | if (e.key === 'ArrowUp') newRow = Math.max(0, rowIndex - 1); 3368 | if (e.key === 'ArrowDown') newRow = Math.min(rows.length - 1, rowIndex + 1); 3369 | if (e.key === 'ArrowLeft') newCol = Math.max(0, colIndex - 1); 3370 | if (e.key === 'ArrowRight') newCol = Math.min(columns.length - 1, colIndex + 1); 3371 | if (e.key === 'Home') newCol = 0; 3372 | if (e.key === 'End') newCol = columns.length - 1; 3373 | 3374 | if (newRow !== rowIndex || newCol !== colIndex) { 3375 | e.preventDefault(); 3376 | setFocusedCell({ row: newRow, col: newCol }); 3377 | cellRefs.current[newRow]?.[newCol]?.focus(); 3378 | } 3379 | }; 3380 | 3381 | return ( 3382 | <table> 3383 | <tbody> 3384 | {rows.map((row, ri) => ( 3385 | <tr key={row.id}> 3386 | {columns.map((col, ci) => ( 3387 | <td 3388 | key={col.id} 3389 | ref={el => { 3390 | if (!cellRefs.current[ri]) cellRefs.current[ri] = []; 3391 | cellRefs.current[ri][ci] = el; 3392 | }} 3393 | tabIndex={focusedCell.row === ri && focusedCell.col === ci ? 0 : -1} 3394 | onKeyDown={e => handleKeyDown(e, ri, ci)} 3395 | onFocus={() => setFocusedCell({ row: ri, col: ci })} 3396 | > 3397 | {row[col.id]} 3398 | </td> 3399 | ))} 3400 | </tr> 3401 | ))} 3402 | </tbody> 3403 | </table> 3404 | ); 3405 | } 3406 | ``` 3407 | 3408 | **Rules:** Support standard shortcuts (Escape, Tab, Arrows), don't override browser/OS shortcuts, provide visible focus feedback, test keyboard-only navigation. 3409 | 3410 | --- 3411 | 3412 | ### Accessibility Best Practices Summary 3413 | 3414 | **Key principles:** 3415 | - Use semantic HTML first (`<button>`, `<nav>`, `<main>`) 3416 | - Add ARIA only when semantic HTML isn't enough 3417 | - All interactive elements must be keyboard accessible 3418 | - Provide visible focus indicators (use `:focus-visible`) 3419 | - Keep ARIA attributes in sync with visual state 3420 | - Restore focus after closing modals/dialogs 3421 | - Test with screen readers and keyboard only 3422 | 3423 | **Testing checklist:** 3424 | - [ ] Navigate entire app with keyboard only 3425 | - [ ] Focus indicators visible and high contrast 3426 | - [ ] Screen reader announces all content correctly 3427 | - [ ] Color not the only state indicator 3428 | - [ ] Text contrast ≥ 4.5:1 for normal text 3429 | - [ ] Interactive elements have accessible names 3430 | - [ ] Form fields have associated labels 3431 | - [ ] Error messages are announced 3432 | 3433 | **Tools:** [axe DevTools](https://www.deque.com/axe/devtools/), [WAVE](https://wave.webaim.org/), [Lighthouse](https://developers.google.com/web/tools/lighthouse), screen readers (NVDA, JAWS, VoiceOver) 3434 | ```