This is page 111 of 181. Use http://codebase.md/xmlui-org/xmlui/tools/vscode/resources/%7Bsrc%7D?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .changeset │ └── config.json ├── .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 │ ├── layout-changes.md │ ├── 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 │ │ ├── resources │ │ │ ├── favicon.ico │ │ │ ├── files │ │ │ │ └── for-download │ │ │ │ └── xmlui │ │ │ │ └── xmlui-standalone.umd.js │ │ │ ├── github.svg │ │ │ ├── llms.txt │ │ │ ├── logo-dark.svg │ │ │ ├── logo.svg │ │ │ ├── pg-popout.svg │ │ │ ├── rss.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 │ │ │ └── PageNotFound.xmlui │ │ ├── config.ts │ │ ├── Main.xmlui │ │ └── themes │ │ └── blog-theme.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 │ ├── components-with-options.md │ ├── containers.md │ ├── data-operations.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 │ ├── 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 │ │ │ ├── OptionContext.ts │ │ │ ├── Select.md │ │ │ ├── Select.module.scss │ │ │ ├── Select.spec.ts │ │ │ ├── Select.tsx │ │ │ ├── SelectContext.tsx │ │ │ └── SelectNative.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 │ │ │ ├── base64-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/src/components-core/rendering/Container.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import type { Dispatch, MutableRefObject, ReactNode, RefObject, SetStateAction } from "react"; 2 | import React, { 3 | cloneElement, 4 | forwardRef, 5 | Fragment, 6 | isValidElement, 7 | memo, 8 | useCallback, 9 | useEffect, 10 | useRef, 11 | useTransition, 12 | } from "react"; 13 | import { useLocation, useNavigate } from "@remix-run/react"; 14 | import { cloneDeep, isArray } from "lodash-es"; 15 | import { composeRefs } from "@radix-ui/react-compose-refs"; 16 | import memoizeOne from "memoize-one"; 17 | 18 | import type { 19 | LookupActionOptions, 20 | LookupAsyncFnInner, 21 | LookupSyncFnInner, 22 | } from "../../abstractions/ActionDefs"; 23 | import type { ComponentDef, ParentRenderContext } from "../../abstractions/ComponentDefs"; 24 | import type { ContainerState } from "../../abstractions/ContainerDefs"; 25 | import type { LayoutContext, RenderChildFn } from "../../abstractions/RendererDefs"; 26 | import type { 27 | ArrowExpression, 28 | ArrowExpressionStatement, 29 | Statement, 30 | } from "../script-runner/ScriptingSourceTree"; 31 | import { 32 | T_ARROW_EXPRESSION, 33 | T_ARROW_EXPRESSION_STATEMENT, 34 | } from "../script-runner/ScriptingSourceTree"; 35 | import type { ContainerDispatcher, MemoedVars } from "../abstractions/ComponentRenderer"; 36 | import { ContainerActionKind } from "./containers"; 37 | import { useAppContext } from "../AppContext"; 38 | import { buildProxy } from "../rendering/buildProxy"; 39 | import type { StatePartChangedFn } from "./ContainerWrapper"; 40 | import type { 41 | ComponentCleanupFn, 42 | ContainerWrapperDef, 43 | RegisterComponentApiFnInner, 44 | } from "../rendering/ContainerWrapper"; 45 | import type { BindingTreeEvaluationContext } from "../script-runner/BindingTreeEvaluationContext"; 46 | import { processStatementQueueAsync } from "../script-runner/process-statement-async"; 47 | import { processStatementQueue } from "../script-runner/process-statement-sync"; 48 | import { extractParam, shouldKeep } from "../utils/extractParam"; 49 | import { useIsomorphicLayoutEffect } from "../utils/hooks"; 50 | import { capitalizeFirstLetter, delay, generatedId, useEvent } from "../utils/misc"; 51 | import { parseHandlerCode, prepareHandlerStatements } from "../utils/statementUtils"; 52 | import { renderChild } from "./renderChild"; 53 | import { useTheme } from "../theming/ThemeContext"; 54 | import { LoaderComponent } from "../LoaderComponent"; 55 | import type { AppContextObject } from "../../abstractions/AppContextDefs"; 56 | import { EMPTY_ARRAY } from "../constants"; 57 | import type { ParsedEventValue } from "../../abstractions/scripting/Compilation"; 58 | import { useApiInterceptorContext } from "../interception/useApiInterceptorContext"; 59 | import { mergeProps } from "../utils/mergeProps"; 60 | 61 | type Props = { 62 | node: ContainerWrapperDef; 63 | resolvedKey?: string; 64 | componentState: ContainerState; 65 | dispatch: ContainerDispatcher; 66 | setVersion: Dispatch<SetStateAction<number>>; 67 | version: number; 68 | statePartChanged: StatePartChangedFn; 69 | registerComponentApi: RegisterComponentApiFnInner; 70 | parentRegisterComponentApi: RegisterComponentApiFnInner; 71 | layoutContextRef: MutableRefObject<LayoutContext | undefined>; 72 | memoedVarsRef: MutableRefObject<MemoedVars>; 73 | isImplicit?: boolean; 74 | parentDispatch: ContainerDispatcher; 75 | parentRenderContext?: ParentRenderContext; 76 | uidInfoRef?: RefObject<Record<string, any>>; 77 | children?: ReactNode; 78 | }; 79 | 80 | // React component to display a view container and implement its behavior 81 | export const Container = memo( 82 | forwardRef(function Container( 83 | { 84 | node, 85 | componentState, 86 | dispatch: containerDispatch, 87 | parentDispatch, 88 | resolvedKey, 89 | version, 90 | setVersion, 91 | statePartChanged, 92 | registerComponentApi: containerRegisterComponentApi, 93 | parentRegisterComponentApi, 94 | layoutContextRef, 95 | parentRenderContext, 96 | memoedVarsRef, 97 | isImplicit, 98 | uidInfoRef: parentUidInfoRef, 99 | children, 100 | ...rest 101 | }: Props, 102 | ref, 103 | ) { 104 | const { apiBoundContainer } = node; 105 | const dispatch = isImplicit ? parentDispatch : containerDispatch; 106 | const registerComponentApi = isImplicit 107 | ? parentRegisterComponentApi 108 | : containerRegisterComponentApi; 109 | 110 | const appContext = useAppContext(); 111 | const { getThemeVar } = useTheme(); 112 | const navigate = useNavigate(); 113 | const location = useLocation(); 114 | 115 | const fnsRef = useRef<Record<symbol, any>>({}); 116 | 117 | const stateRef = useRef(componentState); 118 | //generally bad practise to write ref in render (https://react.dev/learn/referencing-values-with-refs#best-practices-for-refs), but: 119 | // this stateRef is only used in runCodeSync/async functions, which are memoized, so it's safe to use it here (as I know: illesg) 120 | // In case we sync up the stateRef with the componentState in the useEffect/useInsertionEffect/useLayoutEffect, the stateRef would lag behind the componentState 121 | 122 | stateRef.current = componentState; 123 | 124 | const parsedStatementsRef = useRef<Record<string, Array<Statement> | null>>({}); 125 | const [_, startTransition] = useTransition(); 126 | const mountedRef = useRef(true); 127 | 128 | // --- This ref holds a map of promises for each statement execution that cause any state change. 129 | const statementPromises = useRef<Map<string, any>>(new Map()); 130 | 131 | // --- Ensure that re-rendering because of version change resolves all pending statement promises. 132 | useIsomorphicLayoutEffect(() => { 133 | for (const resolve of statementPromises.current.values()) { 134 | resolve(); 135 | } 136 | }, [version]); 137 | 138 | // --- Ensure that component disposal resolves all pending statement promises. 139 | useEffect(() => { 140 | mountedRef.current = true; 141 | const leftPromises = statementPromises.current; 142 | return () => { 143 | mountedRef.current = false; 144 | for (const resolve of leftPromises.values()) { 145 | resolve(); 146 | } 147 | }; 148 | }, []); 149 | 150 | const { apiInstance } = useApiInterceptorContext(); 151 | 152 | const runCodeAsync = useEvent( 153 | async ( 154 | source: string | ParsedEventValue | ArrowExpression, 155 | componentUid: symbol, 156 | options: LookupActionOptions | undefined, 157 | ...eventArgs: any[] 158 | ) => { 159 | // --- Check if the event handler can sign its lifecycle state 160 | const canSignEventLifecycle = () => { 161 | return componentUid.description !== undefined && options?.eventName !== undefined; 162 | }; 163 | 164 | let changes: Array<any> = []; 165 | const getComponentStateClone = () => { 166 | changes.length = 0; 167 | const poj = cloneDeep({ ...stateRef.current, ...(options?.context || {}) }); 168 | poj["$this"] = stateRef.current[componentUid]; 169 | return buildProxy(poj, (changeInfo) => { 170 | const idRoot = (changeInfo.pathArray as string[])?.[0]; 171 | if (idRoot?.startsWith("$")) { 172 | throw new Error("Cannot update a read-only variable"); 173 | } 174 | changes.push(changeInfo); 175 | }); 176 | }; 177 | const evalAppContext = { 178 | ...appContext, 179 | getThemeVar, 180 | }; 181 | const evalContext: BindingTreeEvaluationContext = { 182 | appContext: evalAppContext, 183 | eventArgs, 184 | localContext: getComponentStateClone(), 185 | implicitContextGetter: () => { 186 | return { 187 | uid: componentUid, 188 | state: stateRef.current, 189 | getCurrentState: () => stateRef.current, 190 | dispatch, 191 | appContext: evalAppContext, 192 | apiInstance, 193 | navigate, 194 | location, 195 | lookupAction: (action, uid, actionOptions = {}) => { 196 | return lookupAction(action, uid, { 197 | ...actionOptions, 198 | ephemeral: true, 199 | }); 200 | }, 201 | }; 202 | }, 203 | options: { 204 | defaultToOptionalMemberAccess: 205 | typeof appContext.appGlobals?.defaultToOptionalMemberAccess === "boolean" 206 | ? appContext.appGlobals.defaultToOptionalMemberAccess 207 | : true, 208 | }, 209 | }; 210 | 211 | try { 212 | // --- Prepare the event handler to an arrow expression statement 213 | let statements: Statement[]; 214 | if (typeof source === "string") { 215 | if (!parsedStatementsRef.current[source]) { 216 | parsedStatementsRef.current[source] = prepareHandlerStatements( 217 | parseHandlerCode(source), 218 | evalContext, 219 | ); 220 | } 221 | statements = parsedStatementsRef.current[source]; 222 | } else if (isParsedEventValue(source)) { 223 | const parseId = source.parseId.toString(); 224 | if (!parsedStatementsRef.current[parseId]) { 225 | parsedStatementsRef.current[parseId] = prepareHandlerStatements( 226 | source.statements, 227 | evalContext, 228 | ); 229 | } 230 | statements = parsedStatementsRef.current[parseId]; 231 | } else { 232 | statements = [ 233 | { 234 | type: T_ARROW_EXPRESSION_STATEMENT, 235 | expr: source, //TODO illesg (talk it through why we need to deep clone, it it's omitted, it gets slower every time we run it) 236 | } as ArrowExpressionStatement, 237 | ]; 238 | } 239 | 240 | if (!statements?.length) { 241 | return; 242 | } 243 | 244 | if (canSignEventLifecycle()) { 245 | // --- Sign the event handler has been started 246 | dispatch({ 247 | type: ContainerActionKind.EVENT_HANDLER_STARTED, 248 | payload: { 249 | uid: componentUid, 250 | eventName: options.eventName, 251 | }, 252 | }); 253 | } 254 | let mainThreadBlockingRuns = 0; 255 | evalContext.onStatementCompleted = async (evalContext) => { 256 | if (changes.length) { 257 | mainThreadBlockingRuns = 0; 258 | changes.forEach((change) => { 259 | statePartChanged( 260 | change.pathArray, 261 | cloneDeep(change.newValue), 262 | change.target, 263 | change.action, 264 | ); 265 | }); 266 | let resolve = null; 267 | const stateUpdatedPromise = new Promise((res) => { 268 | resolve = () => { 269 | res(null); 270 | }; 271 | }); 272 | const key = generatedId(); 273 | statementPromises.current.set(key, resolve); 274 | // We use this to tell react that this update is not high-priority. 275 | // If we don't put it to a transition, the whole app would be blocked if we run a long, 276 | // update intensive queue (e.g. an infinite loop which 277 | // increments a counter, see playground example learning/01_Experiments/01_Event_Framework/app ). 278 | // Before this solution, we used a setTimeout(..., 0); hack, but some browsers (chrome especially) 279 | // do some funky stuff with the background tabs (e.g. all the setTimeouts are 280 | // maximized to run in 1 time / minute, doesn't matter if it's timeout is 0) 281 | // As of 2023. June 20, this solution works with backgrounded tabs, too. 282 | startTransition(() => { 283 | setVersion((prev) => prev + 1); 284 | }); 285 | 286 | //TODO this could be a problem - if this container gets unmounted, we still have to wait for the update, 287 | // but in that case this update probably happened in the parent (e.g. a button's event handler removes the whole container 288 | // where the button lives, but it still has some statements to run). 289 | // with this solution the statement execution doesn't stop, and we fallback waiting with a setTimeout(0) 290 | if (mountedRef.current) { 291 | await stateUpdatedPromise; 292 | } else { 293 | await delay(0); 294 | } 295 | statementPromises.current.delete(key); 296 | changes = []; 297 | } else { 298 | //in this else branch normally we block the main thread (we don't wait for any state promise to be resolved), 299 | // so in a long-running (typically infinite loop) situation, where there aren't any changes in the state 300 | // we block the main thread indefinitely... this 'mainThreadBlockingRuns' var solution makes sure that 301 | // we pause in every 100 runs, and let the main thread breath a bit, so it's not frozen for the whole time 302 | // (we clear that counter above, too, where we use a startTransition call to de-prioritize this work) 303 | mainThreadBlockingRuns++; 304 | if (mainThreadBlockingRuns > 100) { 305 | mainThreadBlockingRuns = 0; 306 | await delay(0); 307 | } 308 | } 309 | evalContext.localContext = getComponentStateClone(); 310 | }; 311 | 312 | await processStatementQueueAsync(statements, evalContext); 313 | 314 | if (canSignEventLifecycle()) { 315 | // --- Sign the event handler has successfully completed 316 | dispatch({ 317 | type: ContainerActionKind.EVENT_HANDLER_COMPLETED, 318 | payload: { 319 | uid: componentUid, 320 | eventName: options.eventName, 321 | }, 322 | }); 323 | } 324 | 325 | if (evalContext.mainThread?.blocks?.length) { 326 | return evalContext.mainThread.blocks[evalContext.mainThread.blocks.length - 1] 327 | .returnValue; 328 | } 329 | } catch (e) { 330 | //if we pass down an event handler to a component, we should sign the error once, not in every step of the component chain 331 | // (we use it in the compoundComponent, resolving it's event handlers) 332 | if (options?.signError !== false) { 333 | appContext.signError(e as Error); 334 | } 335 | if (canSignEventLifecycle()) { 336 | dispatch({ 337 | type: ContainerActionKind.EVENT_HANDLER_ERROR, 338 | payload: { 339 | uid: componentUid, 340 | eventName: options.eventName, 341 | error: e, 342 | }, 343 | }); 344 | } 345 | throw e; 346 | } 347 | }, 348 | ); 349 | 350 | const runCodeSync = useCallback( 351 | (arrowExpression: ArrowExpression, ...eventArgs: any[]) => { 352 | const evalContext: BindingTreeEvaluationContext = { 353 | localContext: cloneDeep(stateRef.current), 354 | appContext, 355 | eventArgs, 356 | }; 357 | try { 358 | const arrowStmt = { 359 | type: T_ARROW_EXPRESSION_STATEMENT, 360 | expr: arrowExpression, 361 | } as ArrowExpressionStatement; 362 | 363 | processStatementQueue([arrowStmt], evalContext); 364 | 365 | if (evalContext.mainThread?.blocks?.length) { 366 | return evalContext.mainThread.blocks[evalContext.mainThread.blocks.length - 1] 367 | .returnValue; 368 | } 369 | } catch (e) { 370 | console.error(e); 371 | throw e; 372 | } 373 | }, 374 | [appContext], 375 | ); 376 | 377 | const getOrCreateEventHandlerFn = useEvent( 378 | ( 379 | src: string | ParsedEventValue | ArrowExpression, 380 | uid: symbol, 381 | options?: LookupActionOptions, 382 | ) => { 383 | if (Array.isArray(src)) { 384 | throw new Error("Multiple event handlers are not supported"); 385 | } 386 | 387 | let fnCacheKey: string; 388 | let handler: (...eventArgs: any[]) => Promise<any>; 389 | if (typeof src === "string") { 390 | // --- We have a string event handler 391 | fnCacheKey = `${options?.eventName};${src}`; 392 | handler = (...eventArgs: any[]) => { 393 | return runCodeAsync(src, uid, options, ...cloneDeep(eventArgs)); 394 | }; 395 | } else if (isParsedEventValue(src)) { 396 | // --- We have the syntax tree to execute, no need to cache 397 | fnCacheKey = `${options?.eventName};${src.parseId}`; 398 | handler = (...eventArgs: any[]) => { 399 | return runCodeAsync(src, uid, options, ...cloneDeep(eventArgs)); 400 | }; 401 | } else if (isArrowExpression(src)) { 402 | // --- We have an arrow expression to execute 403 | fnCacheKey = `${options?.eventName};${src.statement.nodeId}`; 404 | handler = (...eventArgs: any[]) => { 405 | return runCodeAsync(src, uid, options, ...cloneDeep(eventArgs)); 406 | }; 407 | } else if ((src as any).type) { 408 | // --- We have an arrow expression to execute 409 | fnCacheKey = `${options?.eventName};${JSON.stringify(src)}`; 410 | handler = (...eventArgs: any[]) => { 411 | return runCodeAsync(src, uid, options, ...cloneDeep(eventArgs)); 412 | }; 413 | } else { 414 | // --- We have an unknown event handler 415 | throw new Error("Invalid event handler"); 416 | } 417 | 418 | //if we have a context or ephemeral event handler, we don't cache it (otherwise we would have stale reference for the context) 419 | if (options?.ephemeral || options?.context) { 420 | return handler; 421 | } 422 | if (!fnsRef.current[uid]?.[fnCacheKey]) { 423 | fnsRef.current[uid] = fnsRef.current[uid] || {}; 424 | fnsRef.current[uid][fnCacheKey] = handler; 425 | } 426 | return fnsRef.current[uid][fnCacheKey]; 427 | }, 428 | ); 429 | 430 | const getOrCreateSyncCallbackFn = useCallback( 431 | (arrowExpression: ArrowExpression, uid: symbol) => { 432 | const fnCacheKey = `sync-callback-${arrowExpression.nodeId}`; 433 | if (!fnsRef.current[uid]?.[fnCacheKey]) { 434 | fnsRef.current[uid] = fnsRef.current[uid] || {}; 435 | fnsRef.current[uid][fnCacheKey] = memoizeOne((arrowExpression) => { 436 | return (...eventArgs: any[]) => { 437 | return runCodeSync(arrowExpression, ...eventArgs); 438 | }; 439 | }); 440 | } 441 | return fnsRef.current[uid][fnCacheKey](arrowExpression); 442 | }, 443 | [runCodeSync], 444 | ); 445 | 446 | const lookupSyncCallback: LookupSyncFnInner = useCallback( 447 | (action, uid) => { 448 | if (!action) { 449 | return undefined; 450 | } 451 | 452 | if (typeof action === "function") { 453 | return action; 454 | } 455 | 456 | // const resolvedAction = extractParam(componentState, action, appContext, true); 457 | if (!action) { 458 | return undefined; 459 | } 460 | 461 | if (typeof action === "function") { 462 | return action; 463 | } 464 | 465 | if (!(action as any)._ARROW_EXPR_) { 466 | throw new Error("Only arrow expression allowed in sync callback"); 467 | } 468 | return getOrCreateSyncCallbackFn(action, uid); 469 | }, 470 | [getOrCreateSyncCallbackFn], 471 | ); 472 | 473 | const lookupAction: LookupAsyncFnInner = useCallback( 474 | ( 475 | action: string | undefined | ArrowExpression, 476 | uid: symbol, 477 | options?: LookupActionOptions, 478 | ) => { 479 | let safeAction = action; 480 | if (!action && uid.description && options?.eventName) { 481 | const handlerFnName = `${uid.description}_on${capitalizeFirstLetter(options?.eventName)}`; 482 | if (componentState[handlerFnName] && componentState[handlerFnName]._ARROW_EXPR_) { 483 | safeAction = componentState[handlerFnName] as ArrowExpression; 484 | } 485 | } 486 | if (!safeAction) { 487 | return undefined; 488 | } 489 | if (typeof safeAction === "function") { 490 | return safeAction; 491 | } 492 | return getOrCreateEventHandlerFn(safeAction, uid, options); 493 | }, 494 | [componentState, getOrCreateEventHandlerFn], 495 | ); 496 | 497 | const isApiRegisteredInnerRef = useRef(false); 498 | useEffect(() => { 499 | if (!node.api) { 500 | return; 501 | } 502 | if (!node.containerUid) { 503 | return; 504 | } 505 | if (isApiRegisteredInnerRef.current) { 506 | return; 507 | } 508 | isApiRegisteredInnerRef.current = true; 509 | const api: Record<string, any> = {}; 510 | const self = Symbol("$self"); 511 | Object.entries(node.api).forEach(([key, value]) => { 512 | api[key] = lookupAction(value as string, self); 513 | }); 514 | if (!isImplicit) { 515 | registerComponentApi(self, api); //we register the api as $self for the compound components, 516 | } 517 | parentRegisterComponentApi(node.containerUid, api); // and register it for the parent component instance 518 | }, [ 519 | lookupAction, 520 | node.api, 521 | node.containerUid, 522 | node.uid, 523 | isImplicit, 524 | parentRegisterComponentApi, 525 | registerComponentApi, 526 | ]); 527 | 528 | const cleanup = useEvent((uid) => { 529 | delete fnsRef.current[uid]; 530 | }); 531 | 532 | // --- The container wraps the `renderChild` function to provide that to the child components 533 | const stableRenderChild: RenderChildFn = useCallback( 534 | (childNode, lc, pRenderContext, uidInfoRef, ref, rest) => { 535 | // TODO: Check if this is a valid use case 536 | if (typeof childNode === "string") { 537 | throw Error("should be resolved for now"); 538 | } 539 | 540 | // --- Make children an array if it's not 541 | const children = isArray(childNode) ? childNode : [childNode]; 542 | 543 | if (!children || !children.length) { 544 | // --- No child, nothing to render 545 | return null; 546 | } 547 | 548 | // --- If there are multiple children, we need to add a `key` to each of them 549 | const wrapWithFragment = children.length > 1; 550 | 551 | // --- Render each child 552 | const renderedChildren = children.map((child, childIndex) => { 553 | if (!child) { 554 | // --- No child, nothing to render: Should not happen 555 | return undefined; 556 | } 557 | 558 | // --- Invoke the jolly-joker `renderChild` function to render the child. Note that 559 | // --- in the context, we pass the `stableRenderChild` function, so the child can 560 | // --- render its children recursively. 561 | const renderedChild = renderChild({ 562 | node: child, 563 | state: componentState, 564 | dispatch, 565 | appContext, 566 | lookupAction, 567 | lookupSyncCallback, 568 | registerComponentApi, 569 | renderChild: stableRenderChild, 570 | statePartChanged: statePartChanged, 571 | layoutContext: lc, 572 | parentRenderContext: pRenderContext, 573 | memoedVarsRef, 574 | cleanup, 575 | uidInfoRef, 576 | }); 577 | 578 | if (renderedChild === undefined) { 579 | // --- No displayable child, nothing to render 580 | return undefined; 581 | } 582 | 583 | // --- Let's process the rendered child 584 | let rendered = renderedChild; 585 | const key = `${childIndex}_${child.uid}`; 586 | 587 | if (wrapWithFragment) { 588 | // --- Add the `key` attribute to the child 589 | if (React.isValidElement(renderedChild)) { 590 | // --- React can display this element 591 | rendered = React.cloneElement(renderedChild, { key }); 592 | } else { 593 | // --- A simple text node (or alike). We need to wrap it in a `Fragment` 594 | rendered = <Fragment key={key}>{renderedChild}</Fragment>; 595 | } 596 | } 597 | 598 | // --- Done. 599 | return rendered; 600 | }); 601 | 602 | // --- At this point we have a React node for each child 603 | if (renderedChildren.length === 1) { 604 | // --- If we have a single (and valid React element) child, we compose its 605 | // --- `ref` with the parent's `ref`. This allows the parent to access the child's 606 | // --- DOM node. Otherwise, we use the child as is. 607 | return ref && renderedChildren[0] && isValidElement(renderedChildren[0]) 608 | ? React.cloneElement(renderedChildren[0], { 609 | ref: composeRefs(ref, (renderedChildren[0] as any).ref), 610 | ...mergeProps(renderedChildren[0].props, rest) 611 | } as any) 612 | : renderedChildren[0]; 613 | } 614 | 615 | // --- Done. 616 | return renderedChildren; 617 | }, 618 | [ 619 | componentState, 620 | dispatch, 621 | appContext, 622 | lookupAction, 623 | lookupSyncCallback, 624 | registerComponentApi, 625 | statePartChanged, 626 | memoedVarsRef, 627 | cleanup, 628 | ], 629 | ); 630 | 631 | // --- Log the component state if you need it for debugging 632 | if ((node.props as any)?.debug) { 633 | console.log(`Container: ${resolvedKey}`, { 634 | componentState, 635 | node, 636 | }); 637 | } 638 | 639 | // --- Use this object to store information about already rendered UIDs. 640 | // --- We do not allow any action, loader, or transform to use the same UID; however (as of now) children 641 | // --- may use the same UID. 642 | const uidInfo: Record<string, string> = {}; 643 | 644 | const thisUidInfoRef = useRef({}); 645 | const uidInfoRef = node.uses === undefined ? parentUidInfoRef : thisUidInfoRef; 646 | 647 | const renderedChildren = stableRenderChild( 648 | node.children, 649 | layoutContextRef?.current, 650 | parentRenderContext, 651 | uidInfoRef, 652 | ref, 653 | rest 654 | ); 655 | 656 | const renderedLoaders = renderLoaders({ 657 | uidInfo, 658 | uidInfoRef, 659 | loaders: node.loaders, 660 | componentState, 661 | memoedVarsRef, 662 | //if it's an api bound container, we always use this container, otherwise use the parent if it's an implicit one 663 | dispatch: apiBoundContainer ? containerDispatch : dispatch, 664 | registerComponentApi: apiBoundContainer 665 | ? containerRegisterComponentApi 666 | : registerComponentApi, 667 | appContext, 668 | lookupAction, 669 | lookupSyncCallback, 670 | cleanup, 671 | }); 672 | //TODO illesg 673 | const containerContent = ( 674 | <> 675 | {renderedLoaders} 676 | {!!children && isValidElement(renderedChildren) 677 | ? cloneElement(renderedChildren, null, children) 678 | : renderedChildren} 679 | </> 680 | ); 681 | return ( 682 | <Fragment 683 | key={ 684 | node.uid 685 | ? `${resolvedKey}>${extractParam(componentState, node.uid, appContext, true)}` 686 | : undefined 687 | } 688 | > 689 | {containerContent} 690 | </Fragment> 691 | ); 692 | }), 693 | ); 694 | 695 | interface LoaderRenderContext { 696 | uidInfo: Record<string, string>; 697 | uidInfoRef: RefObject<Record<string, any>>; 698 | loaders?: ComponentDef[]; 699 | componentState: ContainerState; 700 | dispatch: ContainerDispatcher; 701 | appContext: AppContextObject; 702 | registerComponentApi: RegisterComponentApiFnInner; 703 | lookupAction: LookupAsyncFnInner; 704 | lookupSyncCallback: LookupSyncFnInner; 705 | cleanup: ComponentCleanupFn; 706 | memoedVarsRef: MutableRefObject<MemoedVars>; 707 | } 708 | 709 | function renderLoaders({ 710 | uidInfo, 711 | uidInfoRef, 712 | loaders = EMPTY_ARRAY, 713 | componentState, 714 | dispatch, 715 | appContext, 716 | registerComponentApi, 717 | lookupAction, 718 | lookupSyncCallback, 719 | cleanup, 720 | memoedVarsRef, 721 | }: LoaderRenderContext) { 722 | return loaders.map((loader: ComponentDef) => { 723 | // --- Check for the uniqueness of UIDs 724 | if (loader?.uid) { 725 | if (uidInfo[loader.uid]) { 726 | // --- We have a duplicated ID (another loader) 727 | throw new Error( 728 | `Another ${uidInfo[loader.uid]} definition in this container already uses the uid '${loader.uid}'`, 729 | ); 730 | } 731 | uidInfo[loader.uid] = "loader"; 732 | uidInfoRef.current[loader.uid] = { 733 | type: "loader", 734 | value: "loaderValue", 735 | uid: loader.uid, 736 | }; 737 | } 738 | 739 | // --- Render the current loader 740 | const renderedLoader = renderLoader({ 741 | loader, 742 | componentState, 743 | dispatch, 744 | appContext, 745 | registerComponentApi, 746 | lookupAction, 747 | lookupSyncCallback, 748 | memoedVarsRef, 749 | cleanup, 750 | }); 751 | 752 | // --- Skip loaders with rendering errors 753 | if (renderedLoader === undefined) { 754 | return undefined; 755 | } 756 | 757 | // --- Take care to use a key property for the loader 758 | return <Fragment key={loader.uid}>{renderedLoader}</Fragment>; 759 | }); 760 | 761 | function renderLoader({ 762 | loader, 763 | componentState, 764 | dispatch, 765 | appContext, 766 | registerComponentApi, 767 | lookupAction, 768 | lookupSyncCallback, 769 | cleanup, 770 | memoedVarsRef, 771 | }: { 772 | loader: ComponentDef; 773 | componentState: ContainerState; 774 | dispatch: ContainerDispatcher; 775 | appContext: AppContextObject; 776 | registerComponentApi: RegisterComponentApiFnInner; 777 | lookupAction: LookupAsyncFnInner; 778 | lookupSyncCallback: LookupSyncFnInner; 779 | cleanup: ComponentCleanupFn; 780 | memoedVarsRef: MutableRefObject<MemoedVars>; 781 | }) { 782 | // --- For the sake of avoiding further issues 783 | if (!loader) { 784 | return null; 785 | } 786 | 787 | // --- Evaluate "when" to decide if the loader should be rendered 788 | // --- Render only visible components 789 | if (!shouldKeep(loader.when, componentState, appContext)) { 790 | return null; 791 | } 792 | 793 | // --- Use the loader type's renderer function 794 | return ( 795 | <LoaderComponent 796 | onUnmount={cleanup} 797 | node={loader} 798 | state={componentState} 799 | dispatch={dispatch} 800 | registerComponentApi={registerComponentApi} 801 | lookupAction={lookupAction} 802 | lookupSyncCallback={lookupSyncCallback} 803 | memoedVarsRef={memoedVarsRef} 804 | appContext={appContext} 805 | /> 806 | ); 807 | } 808 | } 809 | 810 | function isParsedEventValue( 811 | value: string | ParsedEventValue | ArrowExpression, 812 | ): value is ParsedEventValue { 813 | return (value as ParsedEventValue).__PARSED === true; 814 | } 815 | 816 | function isArrowExpression( 817 | value: string | ParsedEventValue | ArrowExpression, 818 | ): value is ArrowExpression { 819 | return (value as ArrowExpression).type === T_ARROW_EXPRESSION; 820 | } 821 | ``` -------------------------------------------------------------------------------- /xmlui/src/components/Select/Select.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { expect, test } from "../../testing/fixtures"; 2 | 3 | // ============================================================================= 4 | // BASIC FUNCTIONALITY TESTS 5 | // ============================================================================= 6 | 7 | test.describe("Basic Functionality", () => { 8 | // --- Component rendering 9 | 10 | test("dynamic options displayed with Items component", async ({ 11 | initTestBed, 12 | createSelectDriver, 13 | page, 14 | }) => { 15 | await initTestBed(` 16 | <Select> 17 | <Items data="{['One', 'Two', 'Three']}" > 18 | <Option value="{$itemIndex}" label="{$item}" /> 19 | </Items> 20 | </Select>`); 21 | const driver = await createSelectDriver(); 22 | 23 | await driver.click(); 24 | await expect(page.getByRole("option", { name: "One" })).toBeVisible(); 25 | await expect(page.getByRole("option", { name: "Two" })).toBeVisible(); 26 | await expect(page.getByRole("option", { name: "Three" })).toBeVisible(); 27 | }); 28 | 29 | test("changing selected option in form", async ({ initTestBed, createSelectDriver }) => { 30 | await initTestBed(` 31 | <Form data="{{sel: 'opt1'}}"> 32 | <FormItem testId="mySelect" type="select" bindTo="sel"> 33 | <Option value="opt1" label="first"/> 34 | <Option value="opt2" label="second"/> 35 | <Option value="opt3" label="third"/> 36 | </FormItem> 37 | </Form>`); 38 | const driver = await createSelectDriver("mySelect"); 39 | 40 | await expect(driver.component).toHaveText("first"); 41 | await driver.toggleOptionsVisibility(); 42 | await driver.selectLabel("second"); 43 | await expect(driver.component).toHaveText("second"); 44 | }); 45 | 46 | // --- initialValue prop 47 | 48 | test("initialValue set to first valid value", async ({ page, initTestBed }) => { 49 | await initTestBed(` 50 | <Fragment> 51 | <Select id="mySelect" initialValue="{0}"> 52 | <Option value="{0}" label="Zero"/> 53 | <Option value="{1}" label="One"/> 54 | <Option value="{2}" label="Two"/> 55 | </Select> 56 | <Text testId="text">Selected value: {mySelect.value}</Text> 57 | </Fragment> 58 | `); 59 | await expect(page.getByTestId("text")).toHaveText("Selected value: 0"); 60 | await expect(page.getByText("Zero", { exact: true })).toBeVisible(); 61 | await expect(page.getByText("One", { exact: true })).not.toBeVisible(); 62 | }); 63 | 64 | test("initialValue set to non-existant option", async ({ page, initTestBed }) => { 65 | await initTestBed(` 66 | <Fragment> 67 | <Select id="mySelect" initialValue="{42}"> 68 | <Option value="{0}" label="Zero"/> 69 | <Option value="{1}" label="One"/> 70 | <Option value="{2}" label="Two"/> 71 | </Select> 72 | <Text testId="text">Selected value: {mySelect.value}</Text> 73 | </Fragment> 74 | `); 75 | await expect(page.getByTestId("text")).toHaveText("Selected value: 42"); 76 | }); 77 | 78 | test("reset works with initialValue", async ({ 79 | page, 80 | initTestBed, 81 | createSelectDriver, 82 | createButtonDriver, 83 | }) => { 84 | await initTestBed(` 85 | <Fragment> 86 | <Select id="mySelect" initialValue="{0}"> 87 | <Option value="{0}" label="Zero"/> 88 | <Option value="{1}" label="One"/> 89 | <Option value="{2}" label="Two"/> 90 | </Select> 91 | <Button id="resetBtn" label="reset" onClick="mySelect.reset()"/> 92 | <Text testId="text">Selected value: {mySelect.value}</Text> 93 | </Fragment> 94 | `); 95 | const selectDrv = await createSelectDriver("mySelect"); 96 | await selectDrv.toggleOptionsVisibility(); 97 | await selectDrv.selectLabel("One"); 98 | await expect(page.getByTestId("text")).toHaveText("Selected value: 1"); 99 | const btnDriver = await createButtonDriver("resetBtn"); 100 | await btnDriver.click(); 101 | 102 | await expect(page.getByTestId("text")).toHaveText("Selected value: 0"); 103 | }); 104 | 105 | test("reset works with no intialValue", async ({ 106 | page, 107 | initTestBed, 108 | createSelectDriver, 109 | createButtonDriver, 110 | }) => { 111 | await initTestBed(` 112 | <Fragment> 113 | <Select id="mySelect"> 114 | <Option value="{0}" label="Zero"/> 115 | <Option value="{1}" label="One"/> 116 | <Option value="{2}" label="Two"/> 117 | </Select> 118 | <Button id="resetBtn" label="reset" onClick="mySelect.reset()"/> 119 | <Text testId="text">Selected value: {mySelect.value}</Text> 120 | </Fragment> 121 | `); 122 | const selectDrv = await createSelectDriver("mySelect"); 123 | await selectDrv.toggleOptionsVisibility(); 124 | await selectDrv.selectLabel("One"); 125 | await expect(page.getByTestId("text")).toHaveText("Selected value: 1"); 126 | const btnDriver = await createButtonDriver("resetBtn"); 127 | await btnDriver.click(); 128 | 129 | await expect(page.getByTestId("text")).not.toContainText("1"); 130 | }); 131 | 132 | // --- enabled prop 133 | 134 | test("disabled Select cannot be opened", async ({ page, createSelectDriver, initTestBed }) => { 135 | await initTestBed(` 136 | <Select enabled="{false}"> 137 | <Option value="1" label="One"/> 138 | <Option value="2" label="Two"/> 139 | </Select> 140 | `); 141 | const driver = await createSelectDriver(); 142 | await driver.click({ force: true }); 143 | await expect(page.getByText("One")).not.toBeVisible(); 144 | }); 145 | 146 | // --- readOnly prop 147 | 148 | test("readOnly Select shows options, but value cannot be changed", async ({ 149 | page, 150 | initTestBed, 151 | createSelectDriver, 152 | }) => { 153 | await initTestBed(` 154 | <Select readOnly initialValue="1"> 155 | <Option value="1" label="One"/> 156 | <Option value="2" label="Two"/> 157 | </Select> 158 | `); 159 | const driver = await createSelectDriver(); 160 | await expect(driver.component).toHaveText("One"); 161 | await driver.toggleOptionsVisibility(); 162 | await driver.selectLabel("Two"); 163 | await expect(driver.component).toHaveText("One"); 164 | 165 | // verify dropdown is not visible but value is shown 166 | }); 167 | 168 | test("readOnly multi-Select shows options, but value cannot be changed", async ({ 169 | page, 170 | initTestBed, 171 | createSelectDriver, 172 | }) => { 173 | await initTestBed(` 174 | <Select readOnly initialValue="{[1, 2]}" multiSelect> 175 | <Option value="1" label="One"/> 176 | <Option value="2" label="Two"/> 177 | <Option value="3" label="Three"/> 178 | </Select> 179 | `); 180 | const driver = await createSelectDriver(); 181 | await expect(page.getByText("Three")).not.toBeVisible(); 182 | await expect(page.getByText("One")).toBeVisible(); 183 | await expect(page.getByText("Two")).toBeVisible(); 184 | 185 | await driver.toggleOptionsVisibility(); 186 | await driver.selectLabel("Three"); 187 | 188 | await expect(page.getByText("Three")).not.toBeVisible(); 189 | await expect(page.getByText("One")).toBeVisible(); 190 | await expect(page.getByText("Two")).toBeVisible(); 191 | }); 192 | 193 | test("disabled Option cannot be selected", async ({ initTestBed, createSelectDriver, page }) => { 194 | await initTestBed(` 195 | <Select> 196 | <Option value="1" label="One"/> 197 | <Option value="2" label="Two" enabled="{false}"/> 198 | </Select> 199 | `); 200 | await expect(page.getByRole("option", { name: "One" })).not.toBeVisible(); 201 | await expect(page.getByRole("option", { name: "Two" })).not.toBeVisible(); 202 | const driver = await createSelectDriver(); 203 | await driver.toggleOptionsVisibility(); 204 | await driver.selectLabel("Two"); 205 | await expect(page.getByRole("option", { name: "One" })).toBeVisible(); 206 | await expect(page.getByRole("option", { name: "Two" })).toBeVisible(); 207 | }); 208 | 209 | test( 210 | "clicking label brings up the options", 211 | { tag: "@smoke" }, 212 | async ({ initTestBed, page, createSelectDriver }) => { 213 | await initTestBed(` 214 | <Select label="Choose an option"> 215 | <Option value="1" label="One"/> 216 | <Option value="2" label="Two"/> 217 | </Select> 218 | `); 219 | await page.getByLabel("Choose an option").click(); 220 | await expect(page.getByRole("option", { name: "One" })).toBeVisible(); 221 | await expect(page.getByRole("option", { name: "Two" })).toBeVisible(); 222 | }, 223 | ); 224 | 225 | test("label displayed for selected numeric value", async ({ page, initTestBed }) => { 226 | await initTestBed(` 227 | <Fragment> 228 | <Select initialValue="{0}" > 229 | <Option value="{0}" label="Zero"/> 230 | <Option value="{1}" label="One"/> 231 | <Option value="{2}" label="Two"/> 232 | </Select> 233 | </Fragment> 234 | `); 235 | await expect(page.getByText("Zero")).toBeVisible(); 236 | }); 237 | 238 | // --- autoFocus prop 239 | 240 | test("autoFocus brings the focus to component", async ({ 241 | initTestBed, 242 | page, 243 | createSelectDriver, 244 | }) => { 245 | await initTestBed(` 246 | <Select> 247 | <Option value="1" label="One"/> 248 | <Option value="2" label="Two"/> 249 | </Select> 250 | <Select testId="focused-select" autoFocus> 251 | <Option value="1" label="One"/> 252 | <Option value="2" label="Two"/> 253 | </Select> 254 | `); 255 | const driver = await createSelectDriver("focused-select"); 256 | 257 | await expect(driver.component).toBeFocused(); 258 | }); 259 | 260 | // --- Templates 261 | 262 | test("emptyListTemplate shown when wrapped inside an App component", async ({ 263 | initTestBed, 264 | page, 265 | createSelectDriver, 266 | }) => { 267 | await initTestBed(` 268 | <App> 269 | <Select testId="mySelect"> 270 | <property name="emptyListTemplate"> 271 | <Text value="Nothing to see here!" /> 272 | </property> 273 | </Select> 274 | </App> 275 | `); 276 | const driver = await createSelectDriver("mySelect"); 277 | await driver.click(); 278 | 279 | await expect(page.getByText("Nothing to see here!", { exact: true })).toBeVisible(); 280 | }); 281 | 282 | test("optionTemplate is shown", async ({ initTestBed, page, createSelectDriver }) => { 283 | await initTestBed(` 284 | <Select> 285 | <Items items="{[ 286 | { value: 'opt1', label: 'first' }, 287 | { value: 'opt2', label: 'second' }, 288 | { value: 'opt3', label: 'third' }, 289 | ]}"> 290 | <Option value="{$item.value}" label="{$item.label}"> 291 | <Text>Template for value {$item.value}</Text> 292 | </Option> 293 | </Items> 294 | </Select> 295 | `); 296 | const driver = await createSelectDriver(); 297 | await driver.click(); 298 | await expect(page.getByText("Template for value opt1")).toBeVisible(); 299 | await expect(page.getByText("Template for value opt2")).toBeVisible(); 300 | await expect(page.getByText("Template for value opt3")).toBeVisible(); 301 | }); 302 | 303 | // --- placeholder prop 304 | 305 | test("placeholder is shown", async ({ initTestBed, page, createSelectDriver }) => { 306 | await initTestBed(` 307 | <Select placeholder="Please select an item"> 308 | <Option value="opt1" label="first"/> 309 | <Option value="opt2" label="second"/> 310 | <Option value="opt3" label="third"/> 311 | </Select> 312 | `); 313 | await expect(page.getByText("Please select an item")).toBeVisible(); 314 | }); 315 | 316 | test( 317 | "Option without label and value is not rendered", 318 | { tag: "@smoke" }, 319 | async ({ initTestBed, page, createSelectDriver }) => { 320 | await initTestBed(` 321 | <Select placeholder="Please select an item"> 322 | <Option /> 323 | <Option /> 324 | <Option /> 325 | </Select> 326 | `); 327 | const driver = await createSelectDriver(); 328 | await driver.click(); 329 | await expect(page.getByRole("option")).not.toBeVisible(); 330 | }, 331 | ); 332 | 333 | test( 334 | "Option value defaults to label", 335 | { tag: "@smoke" }, 336 | async ({ initTestBed, page, createSelectDriver }) => { 337 | await initTestBed(` 338 | <Fragment> 339 | <Select id="mySelect"> 340 | <Option label="Zero"/> 341 | <Option label="One"/> 342 | <Option label="Two"/> 343 | </Select> 344 | <Text testId="text">Selected value: {mySelect.value}</Text> 345 | </Fragment> 346 | `); 347 | const driver = await createSelectDriver("mySelect"); 348 | await driver.toggleOptionsVisibility(); 349 | await driver.selectLabel("Zero"); 350 | await expect(page.getByTestId("text")).toHaveText("Selected value: Zero"); 351 | }, 352 | ); 353 | }); 354 | 355 | // ============================================================================= 356 | // LABEL POSITIONING TESTS 357 | // ============================================================================= 358 | 359 | test.describe("Label", () => { 360 | test("labelBreak prop defaults to false", async ({ initTestBed, page, createSelectDriver }) => { 361 | await page.setViewportSize({ width: 300, height: 720 }); 362 | 363 | await initTestBed(` 364 | <Select 365 | label="Dignissimos esse quasi esse cupiditate qui qui. Ut provident ad voluptatem tenetur sit consequuntur. Aliquam nisi fugit ut temporibus itaque ducimus rerum. Dolorem reprehenderit qui adipisci. Ullam harum atque ipsa." 366 | > 367 | <Option value="1" label="One"/> 368 | <Option value="2" label="Two"/> 369 | </Select> 370 | `); 371 | const labelWidth = (await page.getByText("Dignissimos esse quasi").boundingBox()).width; 372 | const select = page.getByRole("button").or(page.getByRole("combobox")).first(); 373 | const { width: selectWidth } = await select.boundingBox(); 374 | expect(labelWidth).toBe(selectWidth); 375 | }); 376 | 377 | test('labelWidth applies with labelPosition="start"', async ({ initTestBed, page }) => { 378 | await page.setViewportSize({ width: 300, height: 720 }); 379 | 380 | await initTestBed(` 381 | <Select label="Dignissimos esse quasi" labelWidth="200px" labelPosition="start" > 382 | <Option value="opt1" label="first"/> 383 | <Option value="opt2" label="second"/> 384 | <Option value="opt3" label="third"/> 385 | <Option value="opt4" label="fourth"/> 386 | <Option value="opt5" label="fifth"/> 387 | </Select> 388 | `); 389 | const labelWidth = (await page.getByText("Dignissimos esse quasi").boundingBox()).width; 390 | expect(labelWidth).toBeGreaterThanOrEqual(200); 391 | }); 392 | }); 393 | 394 | // ============================================================================= 395 | // SEARCHABLE SELECT TESTS 396 | // ============================================================================= 397 | 398 | test.describe("searchable select", () => { 399 | test("placeholder is shown", async ({ initTestBed, page, createSelectDriver }) => { 400 | await initTestBed(` 401 | <Select searchable placeholder="Please select an item"> 402 | <Option value="opt1" label="first"/> 403 | <Option value="opt2" label="second"/> 404 | <Option value="opt3" label="third"/> 405 | </Select> 406 | `); 407 | await expect(page.getByText("Please select an item")).toBeVisible(); 408 | }); 409 | 410 | test("inProgressNotificationMessage shown when inProgress is true", async ({ 411 | initTestBed, 412 | page, 413 | createSelectDriver, 414 | }) => { 415 | await initTestBed(` 416 | <Select searchable inProgress inProgressNotificationMessage="in-progress-msg"> 417 | <Option value="opt1" label="first"/> 418 | <Option value="opt2" label="second"/> 419 | <Option value="opt3" label="third"/> 420 | </Select> 421 | `); 422 | const driver = await createSelectDriver(); 423 | await driver.click(); 424 | await expect(page.getByText("in-progress-msg")).toBeVisible(); 425 | }); 426 | 427 | test("inProgressNotificationMessage not shown when inProgress is false", async ({ 428 | initTestBed, 429 | page, 430 | createSelectDriver, 431 | }) => { 432 | await initTestBed(` 433 | <Select searchable inProgress="false" inProgressNotificationMessage="in-progress-msg"> 434 | <Option value="opt1" label="first"/> 435 | <Option value="opt2" label="second"/> 436 | <Option value="opt3" label="third"/> 437 | </Select> 438 | `); 439 | const driver = await createSelectDriver(); 440 | await driver.click(); 441 | await expect(page.getByText("in-progress-msg")).not.toBeVisible(); 442 | }); 443 | 444 | test( 445 | "search filters option labels", 446 | { tag: "@smoke" }, 447 | async ({ initTestBed, page, createSelectDriver }) => { 448 | await initTestBed(` 449 | <Select searchable> 450 | <Option value="opt1" label="first"/> 451 | <Option value="opt2" label="second"/> 452 | <Option value="opt3" label="third"/> 453 | </Select> 454 | `); 455 | const driver = await createSelectDriver(); 456 | await driver.toggleOptionsVisibility(); 457 | await driver.searchFor("econd"); 458 | const options = await page.getByRole("option").all(); 459 | expect(options).toHaveLength(1); 460 | await expect(options[0]).toHaveText("second"); 461 | }, 462 | ); 463 | }); 464 | 465 | // ============================================================================= 466 | // MULTISELECT TESTS 467 | // ============================================================================= 468 | 469 | test.describe("multiSelect", () => { 470 | test("initialValue='{[0]}' works", async ({ page, initTestBed }) => { 471 | await initTestBed(` 472 | <Fragment> 473 | <Select id="mySelect" initialValue="{[0]}" multiSelect> 474 | <Option value="{0}" label="Zero"/> 475 | <Option value="{1}" label="One"/> 476 | <Option value="{2}" label="Two"/> 477 | </Select> 478 | <Text testId="text">Selected value: {mySelect.value}</Text> 479 | </Fragment> 480 | `); 481 | 482 | await expect(page.getByTestId("text")).toHaveText("Selected value: 0"); 483 | }); 484 | 485 | test("initialValue='{[0,1]}' works", { tag: "@smoke" }, async ({ page, initTestBed }) => { 486 | await initTestBed(` 487 | <Fragment> 488 | <Select id="mySelect" initialValue="{[0,1]}" multiSelect> 489 | <Option value="{0}" label="Zero"/> 490 | <Option value="{1}" label="One"/> 491 | <Option value="{2}" label="Two"/> 492 | </Select> 493 | <Text testId="text">Selected value: {mySelect.value}</Text> 494 | </Fragment> 495 | `); 496 | 497 | await expect(page.getByTestId("text")).toHaveText("Selected value: 0,1"); 498 | }); 499 | 500 | test("select multiple items without closing listbox", async ({ 501 | page, 502 | initTestBed, 503 | createSelectDriver, 504 | }) => { 505 | const { testStateDriver } = await initTestBed(` 506 | <Fragment> 507 | <Select id="mySelect" multiSelect> 508 | <Option value="{0}" label="Zero"/> 509 | <Option value="{1}" label="One"/> 510 | <Option value="{2}" label="Two"/> 511 | </Select> 512 | <Text testId="text">Selected value: {mySelect.value}</Text> 513 | </Fragment> 514 | `); 515 | const selectDrv = await createSelectDriver("mySelect"); 516 | await selectDrv.toggleOptionsVisibility(); 517 | await selectDrv.selectMultipleLabels(["Zero", "One"]); 518 | 519 | /* problem is that the listbox closes after the 1st selection is made */ 520 | await expect(page.getByTestId("text")).toHaveText("Selected value: 0,1"); 521 | }); 522 | 523 | test( 524 | "clicking label brings up the options", 525 | { tag: "@smoke" }, 526 | async ({ initTestBed, page, createSelectDriver }) => { 527 | await initTestBed(` 528 | <Select label="Choose an option" multiSelect> 529 | <Option value="1" label="One"/> 530 | <Option value="2" label="Two"/> 531 | </Select> 532 | `); 533 | await page.getByLabel("Choose an option").click(); 534 | await expect(page.getByRole("option", { name: "One" })).toBeVisible(); 535 | await expect(page.getByRole("option", { name: "Two" })).toBeVisible(); 536 | }, 537 | ); 538 | 539 | test("labelBreak prop defaults to false", async ({ initTestBed, page, createSelectDriver }) => { 540 | await page.setViewportSize({ width: 300, height: 720 }); 541 | 542 | await initTestBed(` 543 | <Select 544 | label="Dignissimos esse quasi esse cupiditate qui qui. Ut provident ad voluptatem tenetur sit consequuntur. Aliquam nisi fugit ut temporibus itaque ducimus rerum. Dolorem reprehenderit qui adipisci. Ullam harum atque ipsa." 545 | multiSelect> 546 | <Option value="1" label="One"/> 547 | <Option value="2" label="Two"/> 548 | </Select> 549 | `); 550 | const labelWidth = (await page.getByText("Dignissimos esse quasi").boundingBox()).width; 551 | const select = page.getByRole("button").or(page.getByRole("combobox")).first(); 552 | const { width: selectWidth } = await select.boundingBox(); 553 | expect(labelWidth).toBe(selectWidth); 554 | }); 555 | 556 | test('labelPosition="start" is left in ltr language', async ({ initTestBed, page }) => { 557 | await initTestBed(` 558 | <Select multiSelect label="hi there" labelPosition="start" labelBreak="false"> 559 | <Option value="1" label="One"/> 560 | <Option value="2" label="Two"/> 561 | </Select> 562 | `); 563 | const { x: labelX } = await page.getByText("hi there").boundingBox(); 564 | const select = page.getByRole("button").or(page.getByRole("combobox")).first(); 565 | const { x: selectX } = await select.boundingBox(); 566 | expect(labelX).toBeLessThan(selectX); 567 | }); 568 | 569 | test('labelPosition="start" is right in rtl language', async ({ initTestBed, page }) => { 570 | await initTestBed(` 571 | <VStack direction="rtl"> 572 | <Select multiSelect label="hi there" labelPosition="start" labelBreak="false"> 573 | <Option value="1" label="One" /> 574 | <Option value="2" label="Two" /> 575 | </Select> 576 | </VStack> 577 | `); 578 | const { x: labelX } = await page.getByText("hi there").boundingBox(); 579 | const select = page.getByRole("button").or(page.getByRole("combobox")).first(); 580 | const { x: selectX } = await select.boundingBox(); 581 | expect(labelX).toBeGreaterThan(selectX); 582 | }); 583 | 584 | test("multiSelect autoFocus brings the focus to component", async ({ 585 | initTestBed, 586 | page, 587 | createSelectDriver, 588 | }) => { 589 | await initTestBed(` 590 | <Select multiSelect> 591 | <Option value="1" label="One"/> 592 | <Option value="2" label="Two"/> 593 | </Select> 594 | <Select testId="focused-select" multmultiSelect autoFocus> 595 | <Option value="1" label="One"/> 596 | <Option value="2" label="Two"/> 597 | </Select> 598 | `); 599 | const driver = await createSelectDriver("focused-select"); 600 | 601 | await expect(driver.component).toBeFocused(); 602 | }); 603 | 604 | test("autoFocus brings the focus to component", async ({ 605 | initTestBed, 606 | page, 607 | createSelectDriver, 608 | }) => { 609 | await initTestBed(` 610 | <Select initialValue="opt1" placeholder="Select..." multiSelect> 611 | <property name="valueTemplate"> 612 | <HStack> 613 | <Text>{$item.value}={$item.label}</Text> 614 | <Button 615 | variant="ghost" 616 | icon="close" 617 | size="xs" 618 | testId="remove-item-btn" 619 | onClick="$itemContext.removeItem()"/> 620 | </HStack> 621 | </property> 622 | <Option value="opt1" label="first"/> 623 | <Option value="opt2" label="second"/> 624 | <Option value="opt3" label="third"/> 625 | </Select> 626 | `); 627 | const driver = await createSelectDriver(); 628 | await driver.toggleOptionsVisibility(); 629 | await driver.selectLabel("first"); 630 | 631 | await expect(page.getByText("opt1=first", { exact: true })).toBeVisible(); 632 | await page.getByTestId("remove-item-btn").click(); 633 | await expect(page.getByText("opt1=first", { exact: true })).not.toBeVisible(); 634 | }); 635 | }); 636 | 637 | // ============================================================================= 638 | // SEARCHABLE MULTISELECT TESTS 639 | // ============================================================================= 640 | 641 | test.describe("searchable multiselect", { tag: "@smoke" }, () => { 642 | test("searching for and selecting 2 items works", async ({ 643 | page, 644 | initTestBed, 645 | createSelectDriver, 646 | }) => { 647 | await initTestBed(` 648 | <Fragment> 649 | <Select id="mySelect" testId="mySelect" multiSelect searchable> 650 | <Option value="{0}" label="Zero"/> 651 | <Option value="{1}" label="One"/> 652 | <Option value="{2}" label="Two"/> 653 | </Select> 654 | <Text testId="text">Selected value: {mySelect.value}</Text> 655 | </Fragment> 656 | `); 657 | const driver = await createSelectDriver("mySelect"); 658 | await driver.toggleOptionsVisibility(); 659 | await driver.selectFirstLabelPostSearh("One"); 660 | await driver.selectFirstLabelPostSearh("Two"); 661 | 662 | await expect(page.getByTestId("text")).toHaveText("Selected value: 1,2"); 663 | }); 664 | }); 665 | 666 | // ============================================================================= 667 | // EVENT HANDLING TESTS 668 | // ============================================================================= 669 | 670 | test.describe("Event Handling", () => { 671 | test("gotFocus event fires on focus", async ({ initTestBed, page }) => { 672 | const { testStateDriver } = await initTestBed(` 673 | <Select onGotFocus="testState = 'focused'"> 674 | <Option value="1" label="One"/> 675 | <Option value="2" label="Two"/> 676 | </Select> 677 | `); 678 | const selectButton = page.getByRole("combobox"); 679 | await selectButton.focus(); 680 | await expect.poll(testStateDriver.testState).toBe("focused"); 681 | }); 682 | 683 | test("gotFocus event fires on label click", async ({ initTestBed, page }) => { 684 | const { testStateDriver } = await initTestBed(` 685 | <Select label="Choose" onGotFocus="testState = 'focused'"> 686 | <Option value="1" label="One"/> 687 | <Option value="2" label="Two"/> 688 | </Select> 689 | `); 690 | await page.getByText("Choose").click(); 691 | await expect.poll(testStateDriver.testState).toBe("focused"); 692 | }); 693 | 694 | test("lostFocus event fires on blur", async ({ initTestBed, page }) => { 695 | const { testStateDriver } = await initTestBed(` 696 | <Select onLostFocus="testState = 'blurred'"> 697 | <Option value="1" label="One"/> 698 | <Option value="2" label="Two"/> 699 | </Select> 700 | `); 701 | const selectButton = page.getByRole("combobox"); 702 | await selectButton.focus(); 703 | await selectButton.blur(); 704 | await expect.poll(testStateDriver.testState).toBe("blurred"); 705 | }); 706 | }); 707 | 708 | // ============================================================================= 709 | // FORM INTEGRATION TESTS 710 | // ============================================================================= 711 | 712 | //this is an upstream issue: https://github.com/radix-ui/primitives/issues/3135 713 | test("initialValue honored when used within Form", async ({ initTestBed, page }) => { 714 | await initTestBed(` 715 | <Form> 716 | <Select id="mySelect" initialValue="opt3"> 717 | <Option value="opt1" label="first"/> 718 | <Option value="opt2" label="second"/> 719 | <Option value="opt3" label="third"/> 720 | </Select> 721 | <Text testId="text">Selected value: {mySelect.value}</Text> 722 | </Form>`); 723 | 724 | await expect(page.getByTestId("text")).toHaveText("Selected value: opt3"); 725 | }); 726 | 727 | // ============================================================================= 728 | // VISUAL STATE TESTS 729 | // ============================================================================= 730 | 731 | test.describe("Visual State", () => { 732 | test("input has correct width in px", async ({ page, initTestBed }) => { 733 | await initTestBed(`<Select width="200px" testId="test"/>`, {}); 734 | 735 | const input = page.getByTestId("test"); 736 | const { width } = await input.boundingBox(); 737 | expect(width).toBe(200); 738 | }); 739 | 740 | test("input with label has correct width in px", async ({ page, initTestBed }) => { 741 | await initTestBed(`<Select width="200px" label="test" testId="test"/>`, {}); 742 | 743 | const input = page.getByTestId("test"); 744 | const { width } = await input.boundingBox(); 745 | expect(width).toBe(200); 746 | }); 747 | 748 | test("input has correct width in %", async ({ page, initTestBed }) => { 749 | await page.setViewportSize({ width: 400, height: 300 }); 750 | await initTestBed(`<Select width="50%" testId="test"/>`, {}); 751 | 752 | const input = page.getByTestId("test"); 753 | const { width } = await input.boundingBox(); 754 | expect(width).toBe(200); 755 | }); 756 | 757 | test("input with label has correct width in %", async ({ page, initTestBed }) => { 758 | await page.setViewportSize({ width: 400, height: 300 }); 759 | await initTestBed(`<Select width="50%" label="test" testId="test"/>`, {}); 760 | 761 | const input = page.getByTestId("test"); 762 | const { width } = await input.boundingBox(); 763 | expect(width).toBe(200); 764 | }); 765 | }); 766 | 767 | // ============================================================================= 768 | // Z-INDEX AND MODAL LAYERING TESTS 769 | // ============================================================================= 770 | 771 | test.describe("Z-Index and Modal Layering", () => { 772 | test("Select dropdown in modal is visible and not covered by modal overlay", async ({ 773 | initTestBed, 774 | page, 775 | createSelectDriver, 776 | }) => { 777 | await initTestBed(` 778 | <Fragment> 779 | <Select testId="select"> 780 | <Option value="stuff1">option 1</Option> 781 | <Option value="stuff2">option 2</Option> 782 | <Button onClick="modal.open()">BLOW UP</Button> 783 | </Select> 784 | <ModalDialog id="modal" title="Example Dialog"> 785 | <Form data="{{ firstName: 'Billy', lastName: 'Bob' }}"> 786 | <FormItem bindTo="firstName" required="true" /> 787 | <FormItem bindTo="lastName" required="true" /> 788 | <FormItem 789 | label="Field to Update" 790 | type="select" 791 | width="200px" 792 | bindTo="fieldToUpdate" 793 | required 794 | initialValue="rate" 795 | testId="modal-select" 796 | > 797 | <Option value="rate">Price</Option> 798 | <Option value="description">Item Description</Option> 799 | <Option value="account_id">Account</Option> 800 | </FormItem> 801 | </Form> 802 | </ModalDialog> 803 | </Fragment> 804 | `); 805 | 806 | const selectDriver = await createSelectDriver("select"); 807 | await selectDriver.click(); 808 | 809 | // Click button to open modal 810 | const blowUpButton = page.getByText("BLOW UP"); 811 | await blowUpButton.click(); 812 | 813 | // Wait for modal to be visible 814 | await expect(page.getByRole("dialog", { name: "Example Dialog" })).toBeVisible(); 815 | 816 | // Open the select in the modal 817 | const modalSelectDriver = await createSelectDriver("modal-select"); 818 | await modalSelectDriver.click(); 819 | 820 | // Check that all options are visible 821 | await expect(page.getByRole("option", { name: "Price" })).toBeVisible(); 822 | await expect(page.getByRole("option", { name: "Item Description" })).toBeVisible(); 823 | await expect(page.getByRole("option", { name: "Account" })).toBeVisible(); 824 | }); 825 | }); 826 | ```