This is page 110 of 182. Use http://codebase.md/xmlui-org/xmlui/mockApiDef.js?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 │ │ │ └── xmlui-logo.svg │ │ ├── serve.json │ │ └── web.config │ ├── scripts │ │ ├── download-latest-xmlui.js │ │ ├── generate-rss.js │ │ ├── get-releases.js │ │ └── utils.js │ ├── src │ │ ├── components │ │ │ ├── BlogOverview.xmlui │ │ │ ├── BlogPage.xmlui │ │ │ ├── Debug.xmlui │ │ │ ├── Headlines.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 │ ├── containers.md │ ├── glossary.md │ ├── index.md │ ├── next │ │ ├── component-dev-guide.md │ │ ├── configuration-management-enhancement-summary.md │ │ ├── documentation-scripts-refactoring-complete-summary.md │ │ ├── documentation-scripts-refactoring-plan.md │ │ ├── duplicate-pattern-extraction-summary.md │ │ ├── error-handling-standardization-summary.md │ │ ├── generating-component-reference.md │ │ ├── index.md │ │ ├── logging-consistency-implementation-summary.md │ │ ├── project-build.md │ │ ├── project-structure.md │ │ ├── theme-context.md │ │ ├── tiptap-design-considerations.md │ │ ├── working-with-code.md │ │ ├── xmlui-runtime-architecture │ │ └── xmlui-wcag-accessibility-report.md │ ├── react-fundamentals.md │ ├── release-method.md │ ├── standalone-app.md │ ├── state-management.md │ ├── ud-components.md │ └── xmlui-repo.md ├── package.json ├── playwright.config.ts ├── scripts │ ├── coverage-only.js │ ├── e2e-test-summary.js │ ├── generate-docs │ │ ├── build-downloads-map.mjs │ │ ├── build-pages-map.mjs │ │ ├── components-config.json │ │ ├── configuration-management.mjs │ │ ├── constants.mjs │ │ ├── create-theme-files.mjs │ │ ├── DocsGenerator.mjs │ │ ├── error-handling.mjs │ │ ├── extensions-config.json │ │ ├── folders.mjs │ │ ├── generate-summary-files.mjs │ │ ├── get-docs.mjs │ │ ├── input-handler.mjs │ │ ├── logger.mjs │ │ ├── logging-standards.mjs │ │ ├── MetadataProcessor.mjs │ │ ├── pattern-utilities.mjs │ │ └── utils.mjs │ ├── get-langserver-metadata.mjs │ ├── inline-links.mjs │ └── README-e2e-summary.md ├── src │ ├── abstractions │ │ ├── _conventions.md │ │ ├── ActionDefs.ts │ │ ├── AppContextDefs.ts │ │ ├── ComponentDefs.ts │ │ ├── ContainerDefs.ts │ │ ├── ExtensionDefs.ts │ │ ├── FunctionDefs.ts │ │ ├── RendererDefs.ts │ │ ├── scripting │ │ │ ├── BlockScope.ts │ │ │ ├── Compilation.ts │ │ │ ├── LogicalThread.ts │ │ │ ├── LoopScope.ts │ │ │ ├── modules.ts │ │ │ ├── ScriptParserError.ts │ │ │ ├── Token.ts │ │ │ ├── TryScope.ts │ │ │ └── TryScopeExp.ts │ │ └── ThemingDefs.ts │ ├── components │ │ ├── _conventions.md │ │ ├── abstractions.ts │ │ ├── Accordion │ │ │ ├── Accordion.md │ │ │ ├── Accordion.module.scss │ │ │ ├── Accordion.spec.ts │ │ │ ├── Accordion.tsx │ │ │ ├── AccordionContext.tsx │ │ │ ├── AccordionItem.tsx │ │ │ ├── AccordionItemNative.tsx │ │ │ └── AccordionNative.tsx │ │ ├── Animation │ │ │ └── AnimationNative.tsx │ │ ├── APICall │ │ │ ├── APICall.md │ │ │ ├── APICall.spec.ts │ │ │ ├── APICall.tsx │ │ │ └── APICallNative.tsx │ │ ├── App │ │ │ ├── App.md │ │ │ ├── App.module.scss │ │ │ ├── App.spec.ts │ │ │ ├── App.tsx │ │ │ ├── AppLayoutContext.ts │ │ │ ├── AppNative.tsx │ │ │ ├── AppStateContext.ts │ │ │ ├── doc-resources │ │ │ │ ├── condensed-sticky.xmlui │ │ │ │ ├── condensed.xmlui │ │ │ │ ├── horizontal-sticky.xmlui │ │ │ │ ├── horizontal.xmlui │ │ │ │ ├── vertical-full-header.xmlui │ │ │ │ ├── vertical-sticky.xmlui │ │ │ │ └── vertical.xmlui │ │ │ ├── IndexerContext.ts │ │ │ ├── LinkInfoContext.ts │ │ │ ├── SearchContext.tsx │ │ │ ├── Sheet.module.scss │ │ │ └── Sheet.tsx │ │ ├── AppHeader │ │ │ ├── AppHeader.md │ │ │ ├── AppHeader.module.scss │ │ │ ├── AppHeader.spec.ts │ │ │ ├── AppHeader.tsx │ │ │ └── AppHeaderNative.tsx │ │ ├── AppState │ │ │ ├── AppState.md │ │ │ ├── AppState.spec.ts │ │ │ ├── AppState.tsx │ │ │ └── AppStateNative.tsx │ │ ├── AutoComplete │ │ │ ├── AutoComplete.md │ │ │ ├── AutoComplete.module.scss │ │ │ ├── AutoComplete.spec.ts │ │ │ ├── AutoComplete.tsx │ │ │ ├── AutoCompleteContext.tsx │ │ │ └── AutoCompleteNative.tsx │ │ ├── Avatar │ │ │ ├── Avatar.md │ │ │ ├── Avatar.module.scss │ │ │ ├── Avatar.spec.ts │ │ │ ├── Avatar.tsx │ │ │ └── AvatarNative.tsx │ │ ├── Backdrop │ │ │ ├── Backdrop.md │ │ │ ├── Backdrop.module.scss │ │ │ ├── Backdrop.spec.ts │ │ │ ├── Backdrop.tsx │ │ │ └── BackdropNative.tsx │ │ ├── Badge │ │ │ ├── Badge.md │ │ │ ├── Badge.module.scss │ │ │ ├── Badge.spec.ts │ │ │ ├── Badge.tsx │ │ │ └── BadgeNative.tsx │ │ ├── Bookmark │ │ │ ├── Bookmark.md │ │ │ ├── Bookmark.module.scss │ │ │ ├── Bookmark.spec.ts │ │ │ ├── Bookmark.tsx │ │ │ └── BookmarkNative.tsx │ │ ├── Breakout │ │ │ ├── Breakout.module.scss │ │ │ ├── Breakout.spec.ts │ │ │ ├── Breakout.tsx │ │ │ └── BreakoutNative.tsx │ │ ├── Button │ │ │ ├── Button-style.spec.ts │ │ │ ├── Button.md │ │ │ ├── Button.module.scss │ │ │ ├── Button.spec.ts │ │ │ ├── Button.tsx │ │ │ └── ButtonNative.tsx │ │ ├── Card │ │ │ ├── Card.md │ │ │ ├── Card.module.scss │ │ │ ├── Card.spec.ts │ │ │ ├── Card.tsx │ │ │ └── CardNative.tsx │ │ ├── Carousel │ │ │ ├── Carousel.md │ │ │ ├── Carousel.module.scss │ │ │ ├── Carousel.spec.ts │ │ │ ├── Carousel.tsx │ │ │ ├── CarouselContext.tsx │ │ │ ├── CarouselItem.tsx │ │ │ ├── CarouselItemNative.tsx │ │ │ └── CarouselNative.tsx │ │ ├── ChangeListener │ │ │ ├── ChangeListener.md │ │ │ ├── ChangeListener.spec.ts │ │ │ ├── ChangeListener.tsx │ │ │ └── ChangeListenerNative.tsx │ │ ├── chart-color-schemes.ts │ │ ├── Charts │ │ │ ├── AreaChart │ │ │ │ ├── AreaChart.md │ │ │ │ ├── AreaChart.spec.ts │ │ │ │ ├── AreaChart.tsx │ │ │ │ └── AreaChartNative.tsx │ │ │ ├── BarChart │ │ │ │ ├── BarChart.md │ │ │ │ ├── BarChart.module.scss │ │ │ │ ├── BarChart.spec.ts │ │ │ │ ├── BarChart.tsx │ │ │ │ └── BarChartNative.tsx │ │ │ ├── DonutChart │ │ │ │ ├── DonutChart.spec.ts │ │ │ │ └── DonutChart.tsx │ │ │ ├── LabelList │ │ │ │ ├── LabelList.spec.ts │ │ │ │ ├── LabelList.tsx │ │ │ │ ├── LabelListNative.module.scss │ │ │ │ └── LabelListNative.tsx │ │ │ ├── Legend │ │ │ │ ├── Legend.spec.ts │ │ │ │ ├── Legend.tsx │ │ │ │ └── LegendNative.tsx │ │ │ ├── LineChart │ │ │ │ ├── LineChart.md │ │ │ │ ├── LineChart.module.scss │ │ │ │ ├── LineChart.spec.ts │ │ │ │ ├── LineChart.tsx │ │ │ │ └── LineChartNative.tsx │ │ │ ├── PieChart │ │ │ │ ├── PieChart.md │ │ │ │ ├── PieChart.spec.ts │ │ │ │ ├── PieChart.tsx │ │ │ │ ├── PieChartNative.module.scss │ │ │ │ └── PieChartNative.tsx │ │ │ ├── RadarChart │ │ │ │ ├── RadarChart.md │ │ │ │ ├── RadarChart.spec.ts │ │ │ │ ├── RadarChart.tsx │ │ │ │ └── RadarChartNative.tsx │ │ │ ├── Tooltip │ │ │ │ ├── TooltipContent.module.scss │ │ │ │ ├── TooltipContent.spec.ts │ │ │ │ └── TooltipContent.tsx │ │ │ └── utils │ │ │ ├── abstractions.ts │ │ │ └── ChartProvider.tsx │ │ ├── Checkbox │ │ │ ├── Checkbox.md │ │ │ ├── Checkbox.spec.ts │ │ │ └── Checkbox.tsx │ │ ├── CodeBlock │ │ │ ├── CodeBlock.module.scss │ │ │ ├── CodeBlock.spec.ts │ │ │ ├── CodeBlock.tsx │ │ │ ├── CodeBlockNative.tsx │ │ │ └── highlight-code.ts │ │ ├── collectedComponentMetadata.ts │ │ ├── ColorPicker │ │ │ ├── ColorPicker.md │ │ │ ├── ColorPicker.module.scss │ │ │ ├── ColorPicker.spec.ts │ │ │ ├── ColorPicker.tsx │ │ │ └── ColorPickerNative.tsx │ │ ├── Column │ │ │ ├── Column.md │ │ │ ├── Column.tsx │ │ │ ├── ColumnNative.tsx │ │ │ ├── doc-resources │ │ │ │ └── list-component-data.js │ │ │ └── TableContext.tsx │ │ ├── component-utils.ts │ │ ├── ComponentProvider.tsx │ │ ├── ComponentRegistryContext.tsx │ │ ├── container-helpers.tsx │ │ ├── ContentSeparator │ │ │ ├── ContentSeparator.md │ │ │ ├── ContentSeparator.module.scss │ │ │ ├── ContentSeparator.spec.ts │ │ │ ├── ContentSeparator.tsx │ │ │ └── ContentSeparatorNative.tsx │ │ ├── DataSource │ │ │ ├── DataSource.md │ │ │ └── DataSource.tsx │ │ ├── DateInput │ │ │ ├── DateInput.md │ │ │ ├── DateInput.module.scss │ │ │ ├── DateInput.spec.ts │ │ │ ├── DateInput.tsx │ │ │ └── DateInputNative.tsx │ │ ├── DatePicker │ │ │ ├── DatePicker.md │ │ │ ├── DatePicker.module.scss │ │ │ ├── DatePicker.spec.ts │ │ │ ├── DatePicker.tsx │ │ │ └── DatePickerNative.tsx │ │ ├── DropdownMenu │ │ │ ├── DropdownMenu.md │ │ │ ├── DropdownMenu.module.scss │ │ │ ├── DropdownMenu.spec.ts │ │ │ ├── DropdownMenu.tsx │ │ │ ├── DropdownMenuNative.tsx │ │ │ ├── MenuItem.md │ │ │ └── SubMenuItem.md │ │ ├── EmojiSelector │ │ │ ├── EmojiSelector.md │ │ │ ├── EmojiSelector.spec.ts │ │ │ ├── EmojiSelector.tsx │ │ │ └── EmojiSelectorNative.tsx │ │ ├── ExpandableItem │ │ │ ├── ExpandableItem.module.scss │ │ │ ├── ExpandableItem.spec.ts │ │ │ ├── ExpandableItem.tsx │ │ │ └── ExpandableItemNative.tsx │ │ ├── FileInput │ │ │ ├── FileInput.md │ │ │ ├── FileInput.module.scss │ │ │ ├── FileInput.spec.ts │ │ │ ├── FileInput.tsx │ │ │ └── FileInputNative.tsx │ │ ├── FileUploadDropZone │ │ │ ├── FileUploadDropZone.md │ │ │ ├── FileUploadDropZone.module.scss │ │ │ ├── FileUploadDropZone.spec.ts │ │ │ ├── FileUploadDropZone.tsx │ │ │ └── FileUploadDropZoneNative.tsx │ │ ├── FlowLayout │ │ │ ├── FlowLayout.md │ │ │ ├── FlowLayout.module.scss │ │ │ ├── FlowLayout.spec.ts │ │ │ ├── FlowLayout.spec.ts-snapshots │ │ │ │ └── Edge-cases-boxShadow-is-not-clipped-1-non-smoke-darwin.png │ │ │ ├── FlowLayout.tsx │ │ │ └── FlowLayoutNative.tsx │ │ ├── Footer │ │ │ ├── Footer.md │ │ │ ├── Footer.module.scss │ │ │ ├── Footer.spec.ts │ │ │ ├── Footer.tsx │ │ │ └── FooterNative.tsx │ │ ├── Form │ │ │ ├── Form.md │ │ │ ├── Form.module.scss │ │ │ ├── Form.spec.ts │ │ │ ├── Form.tsx │ │ │ ├── formActions.ts │ │ │ ├── FormContext.ts │ │ │ └── FormNative.tsx │ │ ├── FormItem │ │ │ ├── FormItem.md │ │ │ ├── FormItem.module.scss │ │ │ ├── FormItem.spec.ts │ │ │ ├── FormItem.tsx │ │ │ ├── FormItemNative.tsx │ │ │ ├── HelperText.module.scss │ │ │ ├── HelperText.tsx │ │ │ ├── ItemWithLabel.tsx │ │ │ └── Validations.ts │ │ ├── FormSection │ │ │ ├── FormSection.md │ │ │ ├── FormSection.ts │ │ │ └── FormSection.xmlui │ │ ├── Fragment │ │ │ ├── Fragment.spec.ts │ │ │ └── Fragment.tsx │ │ ├── Heading │ │ │ ├── abstractions.ts │ │ │ ├── H1.md │ │ │ ├── H1.spec.ts │ │ │ ├── H2.md │ │ │ ├── H2.spec.ts │ │ │ ├── H3.md │ │ │ ├── H3.spec.ts │ │ │ ├── H4.md │ │ │ ├── H4.spec.ts │ │ │ ├── H5.md │ │ │ ├── H5.spec.ts │ │ │ ├── H6.md │ │ │ ├── H6.spec.ts │ │ │ ├── Heading.md │ │ │ ├── Heading.module.scss │ │ │ ├── Heading.spec.ts │ │ │ ├── Heading.tsx │ │ │ └── HeadingNative.tsx │ │ ├── HoverCard │ │ │ ├── HoverCard.tsx │ │ │ └── HovercardNative.tsx │ │ ├── HtmlTags │ │ │ ├── HtmlTags.module.scss │ │ │ ├── HtmlTags.spec.ts │ │ │ └── HtmlTags.tsx │ │ ├── Icon │ │ │ ├── AdmonitionDanger.tsx │ │ │ ├── AdmonitionInfo.tsx │ │ │ ├── AdmonitionNote.tsx │ │ │ ├── AdmonitionTip.tsx │ │ │ ├── AdmonitionWarning.tsx │ │ │ ├── ApiIcon.tsx │ │ │ ├── ArrowDropDown.module.scss │ │ │ ├── ArrowDropDown.tsx │ │ │ ├── ArrowDropUp.module.scss │ │ │ ├── ArrowDropUp.tsx │ │ │ ├── ArrowLeft.module.scss │ │ │ ├── ArrowLeft.tsx │ │ │ ├── ArrowRight.module.scss │ │ │ ├── ArrowRight.tsx │ │ │ ├── Attach.tsx │ │ │ ├── Binding.module.scss │ │ │ ├── Binding.tsx │ │ │ ├── BoardIcon.tsx │ │ │ ├── BoxIcon.tsx │ │ │ ├── CheckIcon.tsx │ │ │ ├── ChevronDownIcon.tsx │ │ │ ├── ChevronLeft.tsx │ │ │ ├── ChevronRight.tsx │ │ │ ├── ChevronUpIcon.tsx │ │ │ ├── CodeFileIcon.tsx │ │ │ ├── CodeSandbox.tsx │ │ │ ├── CompactListIcon.tsx │ │ │ ├── ContentCopyIcon.tsx │ │ │ ├── DarkToLightIcon.tsx │ │ │ ├── DatabaseIcon.module.scss │ │ │ ├── DatabaseIcon.tsx │ │ │ ├── DocFileIcon.tsx │ │ │ ├── DocIcon.tsx │ │ │ ├── DotMenuHorizontalIcon.tsx │ │ │ ├── DotMenuIcon.tsx │ │ │ ├── EmailIcon.tsx │ │ │ ├── EmptyFolderIcon.tsx │ │ │ ├── ErrorIcon.tsx │ │ │ ├── ExpressionIcon.tsx │ │ │ ├── FillPlusCricleIcon.tsx │ │ │ ├── FilterIcon.tsx │ │ │ ├── FolderIcon.tsx │ │ │ ├── GlobeIcon.tsx │ │ │ ├── HomeIcon.tsx │ │ │ ├── HyperLinkIcon.tsx │ │ │ ├── Icon.md │ │ │ ├── Icon.module.scss │ │ │ ├── Icon.spec.ts │ │ │ ├── Icon.tsx │ │ │ ├── IconNative.tsx │ │ │ ├── ImageFileIcon.tsx │ │ │ ├── Inspect.tsx │ │ │ ├── LightToDark.tsx │ │ │ ├── LinkIcon.tsx │ │ │ ├── ListIcon.tsx │ │ │ ├── LooseListIcon.tsx │ │ │ ├── MoonIcon.tsx │ │ │ ├── MoreOptionsIcon.tsx │ │ │ ├── NoSortIcon.tsx │ │ │ ├── PDFIcon.tsx │ │ │ ├── PenIcon.tsx │ │ │ ├── PhoneIcon.tsx │ │ │ ├── PhotoIcon.tsx │ │ │ ├── PlusIcon.tsx │ │ │ ├── SearchIcon.tsx │ │ │ ├── ShareIcon.tsx │ │ │ ├── SortAscendingIcon.tsx │ │ │ ├── SortDescendingIcon.tsx │ │ │ ├── StarsIcon.tsx │ │ │ ├── SunIcon.tsx │ │ │ ├── svg │ │ │ │ ├── admonition_danger.svg │ │ │ │ ├── admonition_info.svg │ │ │ │ ├── admonition_note.svg │ │ │ │ ├── admonition_tip.svg │ │ │ │ ├── admonition_warning.svg │ │ │ │ ├── api.svg │ │ │ │ ├── arrow-dropdown.svg │ │ │ │ ├── arrow-left.svg │ │ │ │ ├── arrow-right.svg │ │ │ │ ├── arrow-up.svg │ │ │ │ ├── attach.svg │ │ │ │ ├── binding.svg │ │ │ │ ├── box.svg │ │ │ │ ├── bulb.svg │ │ │ │ ├── code-file.svg │ │ │ │ ├── code-sandbox.svg │ │ │ │ ├── dark_to_light.svg │ │ │ │ ├── database.svg │ │ │ │ ├── doc.svg │ │ │ │ ├── empty-folder.svg │ │ │ │ ├── expression.svg │ │ │ │ ├── eye-closed.svg │ │ │ │ ├── eye-dark.svg │ │ │ │ ├── eye.svg │ │ │ │ ├── file-text.svg │ │ │ │ ├── filter.svg │ │ │ │ ├── folder.svg │ │ │ │ ├── img.svg │ │ │ │ ├── inspect.svg │ │ │ │ ├── light_to_dark.svg │ │ │ │ ├── moon.svg │ │ │ │ ├── pdf.svg │ │ │ │ ├── photo.svg │ │ │ │ ├── share.svg │ │ │ │ ├── stars.svg │ │ │ │ ├── sun.svg │ │ │ │ ├── trending-down.svg │ │ │ │ ├── trending-level.svg │ │ │ │ ├── trending-up.svg │ │ │ │ ├── txt.svg │ │ │ │ ├── unknown-file.svg │ │ │ │ ├── unlink.svg │ │ │ │ └── xls.svg │ │ │ ├── TableDeleteColumnIcon.tsx │ │ │ ├── TableDeleteRowIcon.tsx │ │ │ ├── TableInsertColumnIcon.tsx │ │ │ ├── TableInsertRowIcon.tsx │ │ │ ├── TrashIcon.tsx │ │ │ ├── TrendingDownIcon.tsx │ │ │ ├── TrendingLevelIcon.tsx │ │ │ ├── TrendingUpIcon.tsx │ │ │ ├── TxtIcon.tsx │ │ │ ├── UnknownFileIcon.tsx │ │ │ ├── UnlinkIcon.tsx │ │ │ ├── UserIcon.tsx │ │ │ ├── WarningIcon.tsx │ │ │ └── XlsIcon.tsx │ │ ├── IconProvider.tsx │ │ ├── IconRegistryContext.tsx │ │ ├── IFrame │ │ │ ├── IFrame.md │ │ │ ├── IFrame.module.scss │ │ │ ├── IFrame.spec.ts │ │ │ ├── IFrame.tsx │ │ │ └── IFrameNative.tsx │ │ ├── Image │ │ │ ├── Image.md │ │ │ ├── Image.module.scss │ │ │ ├── Image.spec.ts │ │ │ ├── Image.tsx │ │ │ └── ImageNative.tsx │ │ ├── Input │ │ │ ├── index.ts │ │ │ ├── InputAdornment.module.scss │ │ │ ├── InputAdornment.tsx │ │ │ ├── InputDivider.module.scss │ │ │ ├── InputDivider.tsx │ │ │ ├── InputLabel.module.scss │ │ │ ├── InputLabel.tsx │ │ │ ├── PartialInput.module.scss │ │ │ └── PartialInput.tsx │ │ ├── InspectButton │ │ │ ├── InspectButton.module.scss │ │ │ └── InspectButton.tsx │ │ ├── Items │ │ │ ├── Items.md │ │ │ ├── Items.spec.ts │ │ │ ├── Items.tsx │ │ │ └── ItemsNative.tsx │ │ ├── Link │ │ │ ├── Link.md │ │ │ ├── Link.module.scss │ │ │ ├── Link.spec.ts │ │ │ ├── Link.tsx │ │ │ └── LinkNative.tsx │ │ ├── List │ │ │ ├── doc-resources │ │ │ │ └── list-component-data.js │ │ │ ├── List.md │ │ │ ├── List.module.scss │ │ │ ├── List.spec.ts │ │ │ ├── List.tsx │ │ │ └── ListNative.tsx │ │ ├── Logo │ │ │ ├── doc-resources │ │ │ │ └── xmlui-logo.svg │ │ │ ├── Logo.md │ │ │ ├── Logo.tsx │ │ │ └── LogoNative.tsx │ │ ├── Markdown │ │ │ ├── CodeText.module.scss │ │ │ ├── CodeText.tsx │ │ │ ├── Markdown.md │ │ │ ├── Markdown.module.scss │ │ │ ├── Markdown.spec.ts │ │ │ ├── Markdown.tsx │ │ │ ├── MarkdownNative.tsx │ │ │ ├── parse-binding-expr.ts │ │ │ └── utils.ts │ │ ├── metadata-helpers.ts │ │ ├── ModalDialog │ │ │ ├── ConfirmationModalContextProvider.tsx │ │ │ ├── Dialog.module.scss │ │ │ ├── Dialog.tsx │ │ │ ├── ModalDialog.md │ │ │ ├── ModalDialog.module.scss │ │ │ ├── ModalDialog.spec.ts │ │ │ ├── ModalDialog.tsx │ │ │ ├── ModalDialogNative.tsx │ │ │ └── ModalVisibilityContext.tsx │ │ ├── NavGroup │ │ │ ├── NavGroup.md │ │ │ ├── NavGroup.module.scss │ │ │ ├── NavGroup.spec.ts │ │ │ ├── NavGroup.tsx │ │ │ ├── NavGroupContext.ts │ │ │ └── NavGroupNative.tsx │ │ ├── NavLink │ │ │ ├── NavLink.md │ │ │ ├── NavLink.module.scss │ │ │ ├── NavLink.spec.ts │ │ │ ├── NavLink.tsx │ │ │ └── NavLinkNative.tsx │ │ ├── NavPanel │ │ │ ├── NavPanel.md │ │ │ ├── NavPanel.module.scss │ │ │ ├── NavPanel.spec.ts │ │ │ ├── NavPanel.tsx │ │ │ └── NavPanelNative.tsx │ │ ├── NestedApp │ │ │ ├── AppWithCodeView.module.scss │ │ │ ├── AppWithCodeView.tsx │ │ │ ├── AppWithCodeViewNative.tsx │ │ │ ├── defaultProps.tsx │ │ │ ├── logo.svg │ │ │ ├── NestedApp.module.scss │ │ │ ├── NestedApp.tsx │ │ │ ├── NestedAppNative.tsx │ │ │ ├── Tooltip.module.scss │ │ │ ├── Tooltip.tsx │ │ │ └── utils.ts │ │ ├── NoResult │ │ │ ├── NoResult.md │ │ │ ├── NoResult.module.scss │ │ │ ├── NoResult.spec.ts │ │ │ ├── NoResult.tsx │ │ │ └── NoResultNative.tsx │ │ ├── NumberBox │ │ │ ├── numberbox-abstractions.ts │ │ │ ├── NumberBox.md │ │ │ ├── NumberBox.module.scss │ │ │ ├── NumberBox.spec.ts │ │ │ ├── NumberBox.tsx │ │ │ └── NumberBoxNative.tsx │ │ ├── Option │ │ │ ├── Option.md │ │ │ ├── Option.spec.ts │ │ │ ├── Option.tsx │ │ │ ├── OptionNative.tsx │ │ │ └── OptionTypeProvider.tsx │ │ ├── PageMetaTitle │ │ │ ├── PageMetaTilteNative.tsx │ │ │ ├── PageMetaTitle.md │ │ │ ├── PageMetaTitle.spec.ts │ │ │ └── PageMetaTitle.tsx │ │ ├── Pages │ │ │ ├── Page.md │ │ │ ├── Pages.md │ │ │ ├── Pages.module.scss │ │ │ ├── Pages.tsx │ │ │ └── PagesNative.tsx │ │ ├── Pagination │ │ │ ├── Pagination.md │ │ │ ├── Pagination.module.scss │ │ │ ├── Pagination.spec.ts │ │ │ ├── Pagination.tsx │ │ │ └── PaginationNative.tsx │ │ ├── PositionedContainer │ │ │ ├── PositionedContainer.module.scss │ │ │ ├── PositionedContainer.tsx │ │ │ └── PositionedContainerNative.tsx │ │ ├── ProfileMenu │ │ │ ├── ProfileMenu.module.scss │ │ │ └── ProfileMenu.tsx │ │ ├── ProgressBar │ │ │ ├── ProgressBar.md │ │ │ ├── ProgressBar.module.scss │ │ │ ├── ProgressBar.spec.ts │ │ │ ├── ProgressBar.tsx │ │ │ └── ProgressBarNative.tsx │ │ ├── Queue │ │ │ ├── Queue.md │ │ │ ├── Queue.spec.ts │ │ │ ├── Queue.tsx │ │ │ ├── queueActions.ts │ │ │ └── QueueNative.tsx │ │ ├── RadioGroup │ │ │ ├── RadioGroup.md │ │ │ ├── RadioGroup.module.scss │ │ │ ├── RadioGroup.spec.ts │ │ │ ├── RadioGroup.tsx │ │ │ ├── RadioGroupNative.tsx │ │ │ ├── RadioItem.tsx │ │ │ └── RadioItemNative.tsx │ │ ├── RealTimeAdapter │ │ │ ├── RealTimeAdapter.tsx │ │ │ └── RealTimeAdapterNative.tsx │ │ ├── Redirect │ │ │ ├── Redirect.md │ │ │ ├── Redirect.spec.ts │ │ │ └── Redirect.tsx │ │ ├── ResponsiveBar │ │ │ ├── README.md │ │ │ ├── ResponsiveBar.md │ │ │ ├── ResponsiveBar.module.scss │ │ │ ├── ResponsiveBar.spec.ts │ │ │ ├── ResponsiveBar.tsx │ │ │ └── ResponsiveBarNative.tsx │ │ ├── Select │ │ │ ├── HiddenOption.tsx │ │ │ ├── MultiSelectOption.tsx │ │ │ ├── OptionContext.ts │ │ │ ├── Select.md │ │ │ ├── Select.module.scss │ │ │ ├── Select.spec.ts │ │ │ ├── Select.tsx │ │ │ ├── SelectContext.tsx │ │ │ ├── SelectNative.tsx │ │ │ ├── SelectOption.tsx │ │ │ └── SimpleSelect.tsx │ │ ├── SelectionStore │ │ │ ├── SelectionStore.md │ │ │ ├── SelectionStore.tsx │ │ │ └── SelectionStoreNative.tsx │ │ ├── Slider │ │ │ ├── Slider.md │ │ │ ├── Slider.module.scss │ │ │ ├── Slider.spec.ts │ │ │ ├── Slider.tsx │ │ │ └── SliderNative.tsx │ │ ├── Slot │ │ │ ├── Slot.md │ │ │ ├── Slot.spec.ts │ │ │ └── Slot.ts │ │ ├── SlotItem.tsx │ │ ├── SpaceFiller │ │ │ ├── SpaceFiller.md │ │ │ ├── SpaceFiller.module.scss │ │ │ ├── SpaceFiller.spec.ts │ │ │ ├── SpaceFiller.tsx │ │ │ └── SpaceFillerNative.tsx │ │ ├── Spinner │ │ │ ├── Spinner.md │ │ │ ├── Spinner.module.scss │ │ │ ├── Spinner.spec.ts │ │ │ ├── Spinner.tsx │ │ │ └── SpinnerNative.tsx │ │ ├── Splitter │ │ │ ├── HSplitter.md │ │ │ ├── HSplitter.spec.ts │ │ │ ├── Splitter.md │ │ │ ├── Splitter.module.scss │ │ │ ├── Splitter.spec.ts │ │ │ ├── Splitter.tsx │ │ │ ├── SplitterNative.tsx │ │ │ ├── utils.ts │ │ │ ├── VSplitter.md │ │ │ └── VSplitter.spec.ts │ │ ├── Stack │ │ │ ├── CHStack.md │ │ │ ├── CHStack.spec.ts │ │ │ ├── CVStack.md │ │ │ ├── CVStack.spec.ts │ │ │ ├── HStack.md │ │ │ ├── HStack.spec.ts │ │ │ ├── Stack.md │ │ │ ├── Stack.module.scss │ │ │ ├── Stack.spec.ts │ │ │ ├── Stack.tsx │ │ │ ├── StackNative.tsx │ │ │ ├── VStack.md │ │ │ └── VStack.spec.ts │ │ ├── StickyBox │ │ │ ├── StickyBox.md │ │ │ ├── StickyBox.module.scss │ │ │ ├── StickyBox.tsx │ │ │ └── StickyBoxNative.tsx │ │ ├── Switch │ │ │ ├── Switch.md │ │ │ ├── Switch.spec.ts │ │ │ └── Switch.tsx │ │ ├── Table │ │ │ ├── doc-resources │ │ │ │ └── list-component-data.js │ │ │ ├── react-table-config.d.ts │ │ │ ├── Table.md │ │ │ ├── Table.module.scss │ │ │ ├── Table.spec.ts │ │ │ ├── Table.tsx │ │ │ ├── TableNative.tsx │ │ │ └── useRowSelection.tsx │ │ ├── TableOfContents │ │ │ ├── TableOfContents.module.scss │ │ │ ├── TableOfContents.spec.ts │ │ │ ├── TableOfContents.tsx │ │ │ └── TableOfContentsNative.tsx │ │ ├── Tabs │ │ │ ├── TabContext.tsx │ │ │ ├── TabItem.md │ │ │ ├── TabItem.tsx │ │ │ ├── TabItemNative.tsx │ │ │ ├── Tabs.md │ │ │ ├── Tabs.module.scss │ │ │ ├── Tabs.spec.ts │ │ │ ├── Tabs.tsx │ │ │ └── TabsNative.tsx │ │ ├── Text │ │ │ ├── Text.md │ │ │ ├── Text.module.scss │ │ │ ├── Text.spec.ts │ │ │ ├── Text.tsx │ │ │ └── TextNative.tsx │ │ ├── TextArea │ │ │ ├── TextArea.md │ │ │ ├── TextArea.module.scss │ │ │ ├── TextArea.spec.ts │ │ │ ├── TextArea.tsx │ │ │ ├── TextAreaNative.tsx │ │ │ ├── TextAreaResizable.tsx │ │ │ └── useComposedRef.ts │ │ ├── TextBox │ │ │ ├── TextBox.md │ │ │ ├── TextBox.module.scss │ │ │ ├── TextBox.spec.ts │ │ │ ├── TextBox.tsx │ │ │ └── TextBoxNative.tsx │ │ ├── Theme │ │ │ ├── NotificationToast.tsx │ │ │ ├── Theme.md │ │ │ ├── Theme.module.scss │ │ │ ├── Theme.spec.ts │ │ │ ├── Theme.tsx │ │ │ └── ThemeNative.tsx │ │ ├── TimeInput │ │ │ ├── TimeInput.md │ │ │ ├── TimeInput.module.scss │ │ │ ├── TimeInput.spec.ts │ │ │ ├── TimeInput.tsx │ │ │ ├── TimeInputNative.tsx │ │ │ └── utils.ts │ │ ├── Timer │ │ │ ├── Timer.md │ │ │ ├── Timer.spec.ts │ │ │ ├── Timer.tsx │ │ │ └── TimerNative.tsx │ │ ├── Toggle │ │ │ ├── Toggle.module.scss │ │ │ └── Toggle.tsx │ │ ├── ToneChangerButton │ │ │ ├── ToneChangerButton.md │ │ │ ├── ToneChangerButton.spec.ts │ │ │ └── ToneChangerButton.tsx │ │ ├── ToneSwitch │ │ │ ├── ToneSwitch.md │ │ │ ├── ToneSwitch.module.scss │ │ │ ├── ToneSwitch.spec.ts │ │ │ ├── ToneSwitch.tsx │ │ │ └── ToneSwitchNative.tsx │ │ ├── Tooltip │ │ │ ├── Tooltip.md │ │ │ ├── Tooltip.module.scss │ │ │ ├── Tooltip.spec.ts │ │ │ ├── Tooltip.tsx │ │ │ └── TooltipNative.tsx │ │ ├── Tree │ │ │ ├── testData.ts │ │ │ ├── Tree-dynamic.spec.ts │ │ │ ├── Tree-icons.spec.ts │ │ │ ├── Tree.md │ │ │ ├── Tree.spec.ts │ │ │ ├── TreeComponent.module.scss │ │ │ ├── TreeComponent.tsx │ │ │ └── TreeNative.tsx │ │ ├── TreeDisplay │ │ │ ├── TreeDisplay.md │ │ │ ├── TreeDisplay.module.scss │ │ │ ├── TreeDisplay.tsx │ │ │ └── TreeDisplayNative.tsx │ │ ├── ValidationSummary │ │ │ ├── ValidationSummary.module.scss │ │ │ └── ValidationSummary.tsx │ │ └── VisuallyHidden.tsx │ ├── components-core │ │ ├── abstractions │ │ │ ├── ComponentRenderer.ts │ │ │ ├── LoaderRenderer.ts │ │ │ ├── standalone.ts │ │ │ └── treeAbstractions.ts │ │ ├── action │ │ │ ├── actions.ts │ │ │ ├── APICall.tsx │ │ │ ├── FileDownloadAction.tsx │ │ │ ├── FileUploadAction.tsx │ │ │ ├── NavigateAction.tsx │ │ │ └── TimedAction.tsx │ │ ├── ApiBoundComponent.tsx │ │ ├── appContext │ │ │ ├── date-functions.ts │ │ │ ├── math-function.ts │ │ │ └── misc-utils.ts │ │ ├── AppContext.tsx │ │ ├── behaviors │ │ │ ├── Behavior.tsx │ │ │ └── CoreBehaviors.tsx │ │ ├── component-hooks.ts │ │ ├── ComponentDecorator.tsx │ │ ├── ComponentViewer.tsx │ │ ├── CompoundComponent.tsx │ │ ├── constants.ts │ │ ├── DebugViewProvider.tsx │ │ ├── descriptorHelper.ts │ │ ├── devtools │ │ │ ├── InspectorDialog.module.scss │ │ │ ├── InspectorDialog.tsx │ │ │ └── InspectorDialogVisibilityContext.tsx │ │ ├── EngineError.ts │ │ ├── event-handlers.ts │ │ ├── InspectorButton.module.scss │ │ ├── InspectorContext.tsx │ │ ├── interception │ │ │ ├── abstractions.ts │ │ │ ├── ApiInterceptor.ts │ │ │ ├── ApiInterceptorProvider.tsx │ │ │ ├── apiInterceptorWorker.ts │ │ │ ├── Backend.ts │ │ │ ├── Errors.ts │ │ │ ├── IndexedDb.ts │ │ │ ├── initMock.ts │ │ │ ├── InMemoryDb.ts │ │ │ ├── ReadonlyCollection.ts │ │ │ └── useApiInterceptorContext.tsx │ │ ├── loader │ │ │ ├── ApiLoader.tsx │ │ │ ├── DataLoader.tsx │ │ │ ├── ExternalDataLoader.tsx │ │ │ ├── Loader.tsx │ │ │ ├── MockLoaderRenderer.tsx │ │ │ └── PageableLoader.tsx │ │ ├── LoaderComponent.tsx │ │ ├── markup-check.ts │ │ ├── parts.ts │ │ ├── renderers.ts │ │ ├── rendering │ │ │ ├── AppContent.tsx │ │ │ ├── AppRoot.tsx │ │ │ ├── AppWrapper.tsx │ │ │ ├── buildProxy.ts │ │ │ ├── collectFnVarDeps.ts │ │ │ ├── ComponentAdapter.tsx │ │ │ ├── ComponentWrapper.tsx │ │ │ ├── Container.tsx │ │ │ ├── containers.ts │ │ │ ├── ContainerWrapper.tsx │ │ │ ├── ErrorBoundary.module.scss │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── InvalidComponent.module.scss │ │ │ ├── InvalidComponent.tsx │ │ │ ├── nodeUtils.ts │ │ │ ├── reducer.ts │ │ │ ├── renderChild.tsx │ │ │ ├── StandaloneComponent.tsx │ │ │ ├── StateContainer.tsx │ │ │ ├── UnknownComponent.module.scss │ │ │ ├── UnknownComponent.tsx │ │ │ └── valueExtractor.ts │ │ ├── reportEngineError.ts │ │ ├── RestApiProxy.ts │ │ ├── script-runner │ │ │ ├── asyncProxy.ts │ │ │ ├── AttributeValueParser.ts │ │ │ ├── bannedFunctions.ts │ │ │ ├── BindingTreeEvaluationContext.ts │ │ │ ├── eval-tree-async.ts │ │ │ ├── eval-tree-common.ts │ │ │ ├── eval-tree-sync.ts │ │ │ ├── ParameterParser.ts │ │ │ ├── process-statement-async.ts │ │ │ ├── process-statement-common.ts │ │ │ ├── process-statement-sync.ts │ │ │ ├── ScriptingSourceTree.ts │ │ │ ├── simplify-expression.ts │ │ │ ├── statement-queue.ts │ │ │ └── visitors.ts │ │ ├── StandaloneApp.tsx │ │ ├── StandaloneExtensionManager.ts │ │ ├── TableOfContentsContext.tsx │ │ ├── theming │ │ │ ├── _themes.scss │ │ │ ├── component-layout-resolver.ts │ │ │ ├── extendThemeUtils.ts │ │ │ ├── hvar.ts │ │ │ ├── layout-resolver.ts │ │ │ ├── parse-layout-props.ts │ │ │ ├── StyleContext.tsx │ │ │ ├── StyleRegistry.ts │ │ │ ├── ThemeContext.tsx │ │ │ ├── ThemeProvider.tsx │ │ │ ├── themes │ │ │ │ ├── base-utils.ts │ │ │ │ ├── palette.ts │ │ │ │ ├── root.ts │ │ │ │ ├── solid.ts │ │ │ │ ├── theme-colors.ts │ │ │ │ └── xmlui.ts │ │ │ ├── themeVars.module.scss │ │ │ ├── themeVars.ts │ │ │ ├── transformThemeVars.ts │ │ │ └── utils.ts │ │ ├── utils │ │ │ ├── actionUtils.ts │ │ │ ├── audio-utils.ts │ │ │ ├── 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/List/List.md: -------------------------------------------------------------------------------- ```markdown 1 | %-DESC-START 2 | 3 | **Key features:** 4 | - **Virtualization**: Renders only visible items for optimal performance with large datasets 5 | - **Advanced grouping**: Group data by any field with customizable headers and footers 6 | - **Built-in sorting**: Sort by any data field in ascending or descending order 7 | - **Visual formatting**: Pre-styled list appearance with borders, spacing, and layout 8 | - **Pagination support**: Handle large datasets with built-in pagination controls 9 | - **Empty state handling**: Customizable templates for when no data is available 10 | 11 | **List vs Items:** 12 | Use `List` for complex data presentation requiring performance optimization, grouping, sorting, or visual formatting. Use `Items` for simple data iteration without layout requirements. 13 | 14 | In the following examples all use the same list of data which looks like so: 15 | 16 | | Id | Name | Quantity | Unit | Category | Key | 17 | | :--- | :------ | :------- | :----- | :--------- | :--- | 18 | | 0 | Apples | 5 | pieces | fruits | 5 | 19 | | 1 | Bananas | 6 | pieces | fruits | 4 | 20 | | 2 | Carrots | 100 | grams | vegetables | 3 | 21 | | 3 | Spinach | 1 | bunch | vegetables | 2 | 22 | | 4 | Milk | 10 | liter | diary | 1 | 23 | | 5 | Cheese | 200 | grams | diary | 0 | 24 | 25 | The data is provided as JSON. 26 | 27 | %-DESC-END 28 | 29 | %-PROP-START data 30 | 31 | Note how the `List` infers the given data and provides a simple layout for it. 32 | To tweak what data and how it is displayed, see the [`itemTemplate` section](#itemtemplate). 33 | 34 | ```xmlui copy 35 | <App> 36 | <List data='{[...]}' /> 37 | </App> 38 | ``` 39 | 40 | ```xmlui-pg name="Example: data" height="400px" 41 | <App> 42 | <List data='{[ 43 | { 44 | id: 0, 45 | name: "Apples", 46 | quantity: 5, 47 | unit: "pieces", 48 | category: "fruits", 49 | key: 5, 50 | }, 51 | { 52 | id: 1, 53 | name: "Bananas", 54 | quantity: 6, 55 | unit: "pieces", 56 | category: "fruits", 57 | key: 4, 58 | }, 59 | { 60 | id: 2, 61 | name: "Carrots", 62 | quantity: 100, 63 | unit: "grams", 64 | category: "vegetables", 65 | key: 3, 66 | }, 67 | { 68 | id: 3, 69 | name: "Spinach", 70 | quantity: 1, 71 | unit: "bunch", 72 | category: "vegetables", 73 | key: 2, 74 | }, 75 | { 76 | id: 4, 77 | name: "Milk", 78 | quantity: 10, 79 | unit: "liter", 80 | category: "dairy", 81 | key: 1, 82 | }, 83 | { 84 | id: 5, 85 | name: "Cheese", 86 | quantity: 200, 87 | unit: "grams", 88 | category: "dairy", 89 | key: 0, 90 | }, 91 | ]}' /> 92 | </App> 93 | ``` 94 | 95 | You can also provide the `List` with data directly from an API via this property. 96 | 97 | In the example below, the `List` also uses the `itemTemplate` property to access the data attributes as well. 98 | See the [itemTemplate section](#itemtemplate). 99 | 100 | ```xmlui-pg copy name="Example: data API Call" height="400px" 101 | <App> 102 | <List data='https://api.spacexdata.com/v3/rockets'> 103 | <property name="itemTemplate"> 104 | <Card> 105 | <Image height="100px" fit="cover" src="{$item.flickr_images[0]}"/> 106 | <Text value="{$item.country}" /> 107 | <Text value="{$item.company}" variant="strong" /> 108 | </Card> 109 | </property> 110 | </List> 111 | </App> 112 | ``` 113 | 114 | %-PROP-END 115 | 116 | %-PROP-START defaultGroups 117 | 118 | >[!INFO] 119 | > For the `defaultGroups` property to work, the data must be sectioned using the [`groupBy`](#groupBy) property, and either a [`groupHeaderTemplate`](#groupHeaderTemplate) or a [`groupFooterTemplate`](#groupFooterTemplate) needs to be provided. 120 | 121 | ```xmlui copy {4} 122 | <App> 123 | <List 124 | data='{[...]}' 125 | defaultGroups="{['dairy', 'meat', 'vegetables']}" 126 | groupBy="category" > 127 | <property name="groupHeaderTemplate"> 128 | <VStack> 129 | <Text variant="subtitle" value="{$group.key}" /> 130 | </VStack> 131 | </property> 132 | </List> 133 | </App> 134 | ``` 135 | 136 | ```xmlui-pg name="Example: defaultGroups" height="400px" 137 | <App> 138 | <List 139 | data='{[ 140 | { 141 | id: 0, 142 | name: "Apples", 143 | quantity: 5, 144 | unit: "pieces", 145 | category: "fruits", 146 | key: 5, 147 | }, 148 | { 149 | id: 1, 150 | name: "Bananas", 151 | quantity: 6, 152 | unit: "pieces", 153 | category: "fruits", 154 | key: 4, 155 | }, 156 | { 157 | id: 2, 158 | name: "Carrots", 159 | quantity: 100, 160 | unit: "grams", 161 | category: "vegetables", 162 | key: 3, 163 | }, 164 | { 165 | id: 3, 166 | name: "Spinach", 167 | quantity: 1, 168 | unit: "bunch", 169 | category: "vegetables", 170 | key: 2, 171 | }, 172 | { 173 | id: 4, 174 | name: "Milk", 175 | quantity: 10, 176 | unit: "liter", 177 | category: "dairy", 178 | key: 1, 179 | }, 180 | { 181 | id: 5, 182 | name: "Cheese", 183 | quantity: 200, 184 | unit: "grams", 185 | category: "dairy", 186 | key: 0, 187 | }, 188 | ]}' 189 | defaultGroups="{['dairy', 'meat', 'vegetables']}" 190 | groupBy="category"> 191 | <property name="groupHeaderTemplate"> 192 | <VStack> 193 | <Text variant="subtitle" value="{$group.key}" /> 194 | </VStack> 195 | </property> 196 | </List> 197 | </App> 198 | ``` 199 | 200 | %-PROP-END 201 | 202 | %-PROP-START emptyListTemplate 203 | 204 | ```xmlui-pg copy display name="Example: emptyListTemplate" height="140px" 205 | <App> 206 | <List> 207 | <property name="emptyListTemplate"> 208 | <VStack horizontalAlignment="center"> 209 | <Text variant="strong" value="Empty..." /> 210 | </VStack> 211 | </property> 212 | </List> 213 | </App> 214 | ``` 215 | 216 | %-PROP-END 217 | 218 | %-PROP-START idKey 219 | 220 | ```xmlui /idKey="key"/ 221 | <App> 222 | <List idKey="key" data='{[...]}' /> 223 | </App> 224 | ``` 225 | 226 | ```xmlui-pg name="Example: idKey" height="400px" 227 | <App> 228 | <List idKey="key" data='{[ 229 | { 230 | id: 0, 231 | name: "Apples", 232 | quantity: 5, 233 | unit: "pieces", 234 | category: "fruits", 235 | key: 5, 236 | }, 237 | { 238 | id: 1, 239 | name: "Bananas", 240 | quantity: 6, 241 | unit: "pieces", 242 | category: "fruits", 243 | key: 4, 244 | }, 245 | { 246 | id: 2, 247 | name: "Carrots", 248 | quantity: 100, 249 | unit: "grams", 250 | category: "vegetables", 251 | key: 3, 252 | }, 253 | { 254 | id: 3, 255 | name: "Spinach", 256 | quantity: 1, 257 | unit: "bunch", 258 | category: "vegetables", 259 | key: 2, 260 | }, 261 | { 262 | id: 4, 263 | name: "Milk", 264 | quantity: 10, 265 | unit: "liter", 266 | category: "dairy", 267 | key: 1, 268 | }, 269 | { 270 | id: 5, 271 | name: "Cheese", 272 | quantity: 200, 273 | unit: "grams", 274 | category: "dairy", 275 | key: 0, 276 | }, 277 | ]}' /> 278 | </App> 279 | ``` 280 | 281 | %-PROP-END 282 | 283 | %-PROP-START itemTemplate 284 | 285 | Note how in the example below the `$item` is used to access the `name`, `quantity` and `unit` attributes. 286 | 287 | ```xmlui copy {3-14} 288 | <App> 289 | <List data='{[...]}'> 290 | <property name="itemTemplate"> 291 | <Card> 292 | <HStack verticalAlignment="center"> 293 | <Icon name="info" /> 294 | <Text value="{$item.name}" variant="strong" /> 295 | </HStack> 296 | <HStack> 297 | <Text value="{$item.quantity}" /> 298 | <Text value="{$item.unit}" variant="em" /> 299 | </HStack> 300 | </Card> 301 | </property> 302 | </List> 303 | </App> 304 | ``` 305 | 306 | ```xmlui-pg name="Example: itemTemplate" height="400px" 307 | <App> 308 | <List data='{[ 309 | { 310 | id: 0, 311 | name: "Apples", 312 | quantity: 5, 313 | unit: "pieces", 314 | category: "fruits", 315 | key: 5, 316 | }, 317 | { 318 | id: 1, 319 | name: "Bananas", 320 | quantity: 6, 321 | unit: "pieces", 322 | category: "fruits", 323 | key: 4, 324 | }, 325 | { 326 | id: 2, 327 | name: "Carrots", 328 | quantity: 100, 329 | unit: "grams", 330 | category: "vegetables", 331 | key: 3, 332 | }, 333 | { 334 | id: 3, 335 | name: "Spinach", 336 | quantity: 1, 337 | unit: "bunch", 338 | category: "vegetables", 339 | key: 2, 340 | }, 341 | { 342 | id: 4, 343 | name: "Milk", 344 | quantity: 10, 345 | unit: "liter", 346 | category: "dairy", 347 | key: 1, 348 | }, 349 | { 350 | id: 5, 351 | name: "Cheese", 352 | quantity: 200, 353 | unit: "grams", 354 | category: "dairy", 355 | key: 0, 356 | }, 357 | ]}'> 358 | <property name="itemTemplate"> 359 | <Card> 360 | <HStack verticalAlignment="center"> 361 | <Icon name="info" /> 362 | <Text value="{$item.name}" variant="strong" /> 363 | </HStack> 364 | <HStack> 365 | <Text value="{$item.quantity}" /> 366 | <Text value="{$item.unit}" variant="em" /> 367 | </HStack> 368 | </Card> 369 | </property> 370 | </List> 371 | </App> 372 | ``` 373 | 374 | %-PROP-END 375 | 376 | %-PROP-START limit 377 | 378 | ```xmlui /limit="4"/ 379 | <App> 380 | <List limit="3" data='{[...]}' /> 381 | </App> 382 | ``` 383 | 384 | ```xmlui-pg name="Example: limit" height="400px" 385 | <App> 386 | <List limit="3" data='{[ 387 | { 388 | id: 0, 389 | name: "Apples", 390 | quantity: 5, 391 | unit: "pieces", 392 | category: "fruits", 393 | key: 5, 394 | }, 395 | { 396 | id: 1, 397 | name: "Bananas", 398 | quantity: 6, 399 | unit: "pieces", 400 | category: "fruits", 401 | key: 4, 402 | }, 403 | { 404 | id: 2, 405 | name: "Carrots", 406 | quantity: 100, 407 | unit: "grams", 408 | category: "vegetables", 409 | key: 3, 410 | }, 411 | { 412 | id: 3, 413 | name: "Spinach", 414 | quantity: 1, 415 | unit: "bunch", 416 | category: "vegetables", 417 | key: 2, 418 | }, 419 | { 420 | id: 4, 421 | name: "Milk", 422 | quantity: 10, 423 | unit: "liter", 424 | category: "dairy", 425 | key: 1, 426 | }, 427 | { 428 | id: 5, 429 | name: "Cheese", 430 | quantity: 200, 431 | unit: "grams", 432 | category: "dairy", 433 | key: 0, 434 | }, 435 | ]}' /> 436 | </App> 437 | ``` 438 | 439 | %-PROP-END 440 | 441 | %-PROP-START loading 442 | 443 | ```xmlui-pg copy display name="Example: loading" height="120px" 444 | <App> 445 | <List loading="true" /> 446 | </App> 447 | ``` 448 | 449 | %-PROP-END 450 | 451 | %-PROP-START orderBy 452 | 453 | ```xmlui /orderBy="{{ field: 'quantity', direction: 'desc' }}"/ 454 | <App> 455 | <List orderBy="{{ field: 'quantity', direction: 'desc' }}" data='{[...]}' /> 456 | </App> 457 | ``` 458 | 459 | ```xmlui-pg name="Example: orderBy" height="400px" 460 | <App> 461 | <List 462 | orderBy="{{ field: 'quantity', direction: 'desc' }}" 463 | data='{[ 464 | { 465 | id: 0, 466 | name: "Apples", 467 | quantity: 5, 468 | unit: "pieces", 469 | category: "fruits", 470 | key: 5, 471 | }, 472 | { 473 | id: 1, 474 | name: "Bananas", 475 | quantity: 6, 476 | unit: "pieces", 477 | category: "fruits", 478 | key: 4, 479 | }, 480 | { 481 | id: 2, 482 | name: "Carrots", 483 | quantity: 100, 484 | unit: "grams", 485 | category: "vegetables", 486 | key: 3, 487 | }, 488 | { 489 | id: 3, 490 | name: "Spinach", 491 | quantity: 1, 492 | unit: "bunch", 493 | category: "vegetables", 494 | key: 2, 495 | }, 496 | { 497 | id: 4, 498 | name: "Milk", 499 | quantity: 10, 500 | unit: "liter", 501 | category: "dairy", 502 | key: 1, 503 | }, 504 | { 505 | id: 5, 506 | name: "Cheese", 507 | quantity: 200, 508 | unit: "grams", 509 | category: "dairy", 510 | key: 0, 511 | }, 512 | ]}' /> 513 | </App> 514 | ``` 515 | 516 | %-PROP-END 517 | 518 | %-PROP-START pageInfo 519 | 520 | It contains the following boolean attributes: 521 | 522 | | Attribute | Description | 523 | | :------------------- | :------------------------------------| 524 | | `hasPrevPage` | Does the list have a previous page | 525 | | `hasNextPage` | Does the list have a next page | 526 | | `isFetchingPrevPage` | _TBD_ | 527 | | `isFetchingNextPage` | _TBD_ | 528 | 529 | %-PROP-END 530 | 531 | %-PROP-START groupBy 532 | 533 | >[!INFO] 534 | > For the `groupBy` property to work, either a [`groupHeaderTemplate`](#groupHeaderTemplate) 535 | > or a [`groupFooterTemplate`](#groupFooterTemplate) needs to be provided. 536 | 537 | ```xmlui copy {3} 538 | <App> 539 | <List 540 | data='{[...]}' 541 | groupBy="category"> 542 | <property name="groupHeaderTemplate"> 543 | <VStack> 544 | <Text variant="subtitle" value="{$group.key}" /> 545 | </VStack> 546 | </property> 547 | </List> 548 | </App> 549 | ``` 550 | 551 | ```xmlui-pg name="Example: groupBy" height="400px" 552 | <App> 553 | <List 554 | data='{[ 555 | { 556 | id: 0, 557 | name: "Apples", 558 | quantity: 5, 559 | unit: "pieces", 560 | category: "fruits", 561 | key: 5, 562 | }, 563 | { 564 | id: 1, 565 | name: "Bananas", 566 | quantity: 6, 567 | unit: "pieces", 568 | category: "fruits", 569 | key: 4, 570 | }, 571 | { 572 | id: 2, 573 | name: "Carrots", 574 | quantity: 100, 575 | unit: "grams", 576 | category: "vegetables", 577 | key: 3, 578 | }, 579 | { 580 | id: 3, 581 | name: "Spinach", 582 | quantity: 1, 583 | unit: "bunch", 584 | category: "vegetables", 585 | key: 2, 586 | }, 587 | { 588 | id: 4, 589 | name: "Milk", 590 | quantity: 10, 591 | unit: "liter", 592 | category: "dairy", 593 | key: 1, 594 | }, 595 | { 596 | id: 5, 597 | name: "Cheese", 598 | quantity: 200, 599 | unit: "grams", 600 | category: "dairy", 601 | key: 0, 602 | }, 603 | ]}' 604 | groupBy="category"> 605 | <property name="groupHeaderTemplate"> 606 | <VStack> 607 | <Text variant="subtitle" value="{$group.key}" /> 608 | </VStack> 609 | </property> 610 | </List> 611 | </App> 612 | ``` 613 | 614 | %-PROP-END 615 | 616 | %-PROP-START groupFooterTemplate 617 | 618 | The structure of `$group` in a `groupFooterTemplate` is the following: 619 | 620 | | Attribute | Description | 621 | | --------- | ------------------------------------------------------------------------------------------------------------- | 622 | | id | Unique identifier for the section. It is commonly generated from the attribute name provided via `groupBy`. | 623 | | items | The items filtered from the original data list that fall into this section. | 624 | | key | The attribute name to section by provided via `groupBy` | 625 | 626 | This example displays a separator line in the groups' footer: 627 | 628 | ```xmlui copy {8-12} 629 | <App> 630 | <List data='{[...]}' groupBy="category"> 631 | <property name="groupHeaderTemplate"> 632 | <VStack> 633 | <Text variant="subtitle" value="{$group.key}" /> 634 | </VStack> 635 | </property> 636 | <property name="groupFooterTemplate"> 637 | <VStack paddingVertical="$space-normal"> 638 | <ContentSeparator/> 639 | </VStack> 640 | </property> 641 | </List> 642 | </App> 643 | ``` 644 | 645 | ```xmlui-pg name="Example: groupFooterTemplate" height="400px" 646 | <App> 647 | <List data='{[ 648 | { 649 | id: 0, 650 | name: "Apples", 651 | quantity: 5, 652 | unit: "pieces", 653 | category: "fruits", 654 | key: 5, 655 | }, 656 | { 657 | id: 1, 658 | name: "Bananas", 659 | quantity: 6, 660 | unit: "pieces", 661 | category: "fruits", 662 | key: 4, 663 | }, 664 | { 665 | id: 2, 666 | name: "Carrots", 667 | quantity: 100, 668 | unit: "grams", 669 | category: "vegetables", 670 | key: 3, 671 | }, 672 | { 673 | id: 3, 674 | name: "Spinach", 675 | quantity: 1, 676 | unit: "bunch", 677 | category: "vegetables", 678 | key: 2, 679 | }, 680 | { 681 | id: 4, 682 | name: "Milk", 683 | quantity: 10, 684 | unit: "liter", 685 | category: "dairy", 686 | key: 1, 687 | }, 688 | { 689 | id: 5, 690 | name: "Cheese", 691 | quantity: 200, 692 | unit: "grams", 693 | category: "dairy", 694 | key: 0, 695 | }, 696 | ]}' 697 | groupBy="category"> 698 | <property name="groupHeaderTemplate"> 699 | <VStack> 700 | <Text variant="subtitle" value="{$group.key}" /> 701 | </VStack> 702 | </property> 703 | <property name="groupFooterTemplate"> 704 | <VStack paddingVertical="$space-normal"> 705 | <ContentSeparator/> 706 | </VStack> 707 | </property> 708 | </List> 709 | </App> 710 | ``` 711 | 712 | %-PROP-END 713 | 714 | %-PROP-START groupHeaderTemplate 715 | 716 | The structure of `$group` in a `groupHeaderTemplate` is the following: 717 | 718 | | Attribute | Description | 719 | | --------- | ------------------------------------------------------------------------------------------------------------- | 720 | | id | Unique identifier for the section. It is commonly generated from the attribute name provided via `groupBy`. | 721 | | items | The items filtered from the original data list that fall into this section. | 722 | | key | The attribute name to section by provided via `groupBy` | 723 | 724 | ```xmlui copy {3-7} 725 | <App> 726 | <List data='{[...]}' groupBy="category"> 727 | <property name="groupHeaderTemplate"> 728 | <Stack padding="$space-2"> 729 | <Text variant="subtitle" value="{$group.key}" /> 730 | </Stack> 731 | </property> 732 | </List> 733 | </App> 734 | ``` 735 | 736 | ```xmlui-pg copy name="Example: groupHeaderTemplate" height="400px" 737 | <App> 738 | <List data='{[ 739 | { 740 | id: 0, 741 | name: "Apples", 742 | quantity: 5, 743 | unit: "pieces", 744 | category: "fruits", 745 | key: 5, 746 | }, 747 | { 748 | id: 1, 749 | name: "Bananas", 750 | quantity: 6, 751 | unit: "pieces", 752 | category: "fruits", 753 | key: 4, 754 | }, 755 | { 756 | id: 2, 757 | name: "Carrots", 758 | quantity: 100, 759 | unit: "grams", 760 | category: "vegetables", 761 | key: 3, 762 | }, 763 | { 764 | id: 3, 765 | name: "Spinach", 766 | quantity: 1, 767 | unit: "bunch", 768 | category: "vegetables", 769 | key: 2, 770 | }, 771 | { 772 | id: 4, 773 | name: "Milk", 774 | quantity: 10, 775 | unit: "liter", 776 | category: "dairy", 777 | key: 1, 778 | }, 779 | { 780 | id: 5, 781 | name: "Cheese", 782 | quantity: 200, 783 | unit: "grams", 784 | category: "dairy", 785 | key: 0, 786 | }, 787 | ]}' 788 | groupBy="category"> 789 | <property name="groupHeaderTemplate"> 790 | <Stack padding="$space-2"> 791 | <Text variant="subtitle" value="{$group.key}" /> 792 | </Stack> 793 | </property> 794 | </List> 795 | </App> 796 | ``` 797 | 798 | %-PROP-END 799 | 800 | %-PROP-START selectedIndex 801 | 802 | ```xmlui /selectedIndex="5"/ 803 | <App> 804 | <List selectedIndex="5" data='{[...]}' /> 805 | </App> 806 | ``` 807 | 808 | ```xmlui-pg name="Example: selectedIndex" height="400px" 809 | <App> 810 | <List selectedIndex="5" data='{[ 811 | { 812 | id: 0, 813 | name: "Apples", 814 | quantity: 5, 815 | unit: "pieces", 816 | category: "fruits", 817 | key: 5, 818 | }, 819 | { 820 | id: 1, 821 | name: "Bananas", 822 | quantity: 6, 823 | unit: "pieces", 824 | category: "fruits", 825 | key: 4, 826 | }, 827 | { 828 | id: 2, 829 | name: "Carrots", 830 | quantity: 100, 831 | unit: "grams", 832 | category: "vegetables", 833 | key: 3, 834 | }, 835 | { 836 | id: 3, 837 | name: "Spinach", 838 | quantity: 1, 839 | unit: "bunch", 840 | category: "vegetables", 841 | key: 2, 842 | }, 843 | { 844 | id: 4, 845 | name: "Milk", 846 | quantity: 10, 847 | unit: "liter", 848 | category: "dairy", 849 | key: 1, 850 | }, 851 | { 852 | id: 5, 853 | name: "Cheese", 854 | quantity: 200, 855 | unit: "grams", 856 | category: "dairy", 857 | key: 0, 858 | }, 859 | ]}' /> 860 | </App> 861 | ``` 862 | 863 | %-PROP-END 864 | 865 | %-PROP-START scrollAnchor 866 | 867 | ```xmlui /scrollAnchor="bottom"/ 868 | <App> 869 | <List scrollAnchor="bottom" data='{[...]}' height="300px" /> 870 | </App> 871 | ``` 872 | 873 | ```xmlui-pg name="Example: scrollAnchor" height="380px" 874 | <App> 875 | <List scrollAnchor="bottom" height="300px" data='{[ 876 | { 877 | id: 0, 878 | name: "Apples", 879 | quantity: 5, 880 | unit: "pieces", 881 | category: "fruits", 882 | key: 5, 883 | }, 884 | { 885 | id: 1, 886 | name: "Bananas", 887 | quantity: 6, 888 | unit: "pieces", 889 | category: "fruits", 890 | key: 4, 891 | }, 892 | { 893 | id: 2, 894 | name: "Carrots", 895 | quantity: 100, 896 | unit: "grams", 897 | category: "vegetables", 898 | key: 3, 899 | }, 900 | { 901 | id: 3, 902 | name: "Spinach", 903 | quantity: 1, 904 | unit: "bunch", 905 | category: "vegetables", 906 | key: 2, 907 | }, 908 | { 909 | id: 4, 910 | name: "Milk", 911 | quantity: 10, 912 | unit: "liter", 913 | category: "dairy", 914 | key: 1, 915 | }, 916 | { 917 | id: 5, 918 | name: "Cheese", 919 | quantity: 200, 920 | unit: "grams", 921 | category: "dairy", 922 | key: 0, 923 | }, 924 | ]}' /> 925 | </App> 926 | ``` 927 | 928 | %-PROP-END 929 | 930 | %-PROP-START availableGroups 931 | 932 | ```xmlui {5} 933 | <App> 934 | <List 935 | data="{[...]}" 936 | groupBy="category" 937 | availableGroups="{['fruits', 'vegetables']}"> 938 | <property name="groupHeaderTemplate"> 939 | <Stack> 940 | <Text variant="subtitle" value="{$group.key}" /> 941 | </Stack> 942 | </property> 943 | </List> 944 | </App> 945 | ``` 946 | 947 | ```xmlui-pg name="Example: availableGroups" height="400px" 948 | <App> 949 | <List availableGroups="{['fruits', 'vegetables']}" groupBy="category" 950 | data='{[ 951 | { 952 | id: 0, 953 | name: "Apples", 954 | quantity: 5, 955 | unit: "pieces", 956 | category: "fruits", 957 | key: 5, 958 | }, 959 | { 960 | id: 1, 961 | name: "Bananas", 962 | quantity: 6, 963 | unit: "pieces", 964 | category: "fruits", 965 | key: 4, 966 | }, 967 | { 968 | id: 2, 969 | name: "Carrots", 970 | quantity: 100, 971 | unit: "grams", 972 | category: "vegetables", 973 | key: 3, 974 | }, 975 | { 976 | id: 3, 977 | name: "Spinach", 978 | quantity: 1, 979 | unit: "bunch", 980 | category: "vegetables", 981 | key: 2, 982 | }, 983 | { 984 | id: 4, 985 | name: "Milk", 986 | quantity: 10, 987 | unit: "liter", 988 | category: "dairy", 989 | key: 1, 990 | }, 991 | { 992 | id: 5, 993 | name: "Cheese", 994 | quantity: 200, 995 | unit: "grams", 996 | category: "dairy", 997 | key: 0, 998 | }, 999 | ]}'> 1000 | <property name="groupHeaderTemplate"> 1001 | <Stack> 1002 | <Text variant="subtitle" value="{$group.key}" /> 1003 | </Stack> 1004 | </property> 1005 | </List> 1006 | </App> 1007 | ``` 1008 | 1009 | %-PROP-END 1010 | 1011 | %-PROP-START borderCollapse 1012 | 1013 | Note how the `List` on the right has different borders: 1014 | 1015 | ```xmlui /borderCollapse/ 1016 | <App> 1017 | <HStack> 1018 | <List data="{[...]}" groupBy="category" borderCollapse="false" width="$space-48"> 1019 | <property name="groupHeaderTemplate"> 1020 | <Stack> 1021 | <Text variant="subtitle" value="{$group.key}" /> 1022 | </Stack> 1023 | </property> 1024 | </List> 1025 | <List data="{[...]}" groupBy="category" borderCollapse="true" width="$space-48"> 1026 | <property name="groupHeaderTemplate"> 1027 | <Stack> 1028 | <Text variant="subtitle" value="{$group.key}" /> 1029 | </Stack> 1030 | </property> 1031 | </List> 1032 | </HStack> 1033 | </App> 1034 | ``` 1035 | 1036 | ```xmlui-pg name="Example: borderCollapse" height="400px" 1037 | <App> 1038 | <HStack> 1039 | <List data='{[ 1040 | { 1041 | id: 0, 1042 | name: "Apples", 1043 | quantity: 5, 1044 | unit: "pieces", 1045 | category: "fruits", 1046 | key: 5, 1047 | }, 1048 | { 1049 | id: 1, 1050 | name: "Bananas", 1051 | quantity: 6, 1052 | unit: "pieces", 1053 | category: "fruits", 1054 | key: 4, 1055 | }, 1056 | { 1057 | id: 2, 1058 | name: "Carrots", 1059 | quantity: 100, 1060 | unit: "grams", 1061 | category: "vegetables", 1062 | key: 3, 1063 | }, 1064 | { 1065 | id: 3, 1066 | name: "Spinach", 1067 | quantity: 1, 1068 | unit: "bunch", 1069 | category: "vegetables", 1070 | key: 2, 1071 | }, 1072 | { 1073 | id: 4, 1074 | name: "Milk", 1075 | quantity: 10, 1076 | unit: "liter", 1077 | category: "dairy", 1078 | key: 1, 1079 | }, 1080 | { 1081 | id: 5, 1082 | name: "Cheese", 1083 | quantity: 200, 1084 | unit: "grams", 1085 | category: "dairy", 1086 | key: 0, 1087 | }, 1088 | ]}' 1089 | groupBy="category" borderCollapse="false" width="$space-48"> 1090 | <property name="groupHeaderTemplate"> 1091 | <Stack> 1092 | <Text variant="subtitle" value="{$group.key}" /> 1093 | </Stack> 1094 | </property> 1095 | </List> 1096 | <List data='{[ 1097 | { 1098 | id: 0, 1099 | name: "Apples", 1100 | quantity: 5, 1101 | unit: "pieces", 1102 | category: "fruits", 1103 | key: 5, 1104 | }, 1105 | { 1106 | id: 1, 1107 | name: "Bananas", 1108 | quantity: 6, 1109 | unit: "pieces", 1110 | category: "fruits", 1111 | key: 4, 1112 | }, 1113 | { 1114 | id: 2, 1115 | name: "Carrots", 1116 | quantity: 100, 1117 | unit: "grams", 1118 | category: "vegetables", 1119 | key: 3, 1120 | }, 1121 | { 1122 | id: 3, 1123 | name: "Spinach", 1124 | quantity: 1, 1125 | unit: "bunch", 1126 | category: "vegetables", 1127 | key: 2, 1128 | }, 1129 | { 1130 | id: 4, 1131 | name: "Milk", 1132 | quantity: 10, 1133 | unit: "liter", 1134 | category: "dairy", 1135 | key: 1, 1136 | }, 1137 | { 1138 | id: 5, 1139 | name: "Cheese", 1140 | quantity: 200, 1141 | unit: "grams", 1142 | category: "dairy", 1143 | key: 0, 1144 | }, 1145 | ]}' 1146 | groupBy="category" borderCollapse="true" width="$space-48"> 1147 | <property name="groupHeaderTemplate"> 1148 | <Stack> 1149 | <Text variant="subtitle" value="{$group.key}" /> 1150 | </Stack> 1151 | </property> 1152 | </List> 1153 | </HStack> 1154 | </App> 1155 | ``` 1156 | 1157 | %-PROP-END 1158 | 1159 | %-PROP-START groupsInitiallyExpanded 1160 | 1161 | Note how the groups in the right `List` are expanded by default: 1162 | 1163 | ```xmlui /groupsInitiallyExpanded/ 1164 | <App> 1165 | <HStack gap="$space-2"> 1166 | <List data="{[...]}" 1167 | groupBy="category" 1168 | groupsInitiallyExpanded="false" 1169 | width="$space-48"> 1170 | <property name="groupHeaderTemplate"> 1171 | <Stack> 1172 | <Text variant="subtitle" value="{$group.key}" /> 1173 | </Stack> 1174 | </property> 1175 | </List> 1176 | <List data="{[...]}" 1177 | groupBy="category" 1178 | groupsInitiallyExpanded="true" 1179 | width="$space-48"> 1180 | <property name="groupHeaderTemplate"> 1181 | <Stack> 1182 | <Text variant="subtitle" value="{$group.key}" /> 1183 | </Stack> 1184 | </property> 1185 | </List> 1186 | </HStack> 1187 | </App> 1188 | ``` 1189 | 1190 | ```xmlui-pg name="Example: groupsInitiallyExpanded" height="400px" 1191 | <App> 1192 | <HStack gap="$space-2"> 1193 | <List data='{[ 1194 | { 1195 | id: 0, 1196 | name: "Apples", 1197 | quantity: 5, 1198 | unit: "pieces", 1199 | category: "fruits", 1200 | key: 5, 1201 | }, 1202 | { 1203 | id: 1, 1204 | name: "Bananas", 1205 | quantity: 6, 1206 | unit: "pieces", 1207 | category: "fruits", 1208 | key: 4, 1209 | }, 1210 | { 1211 | id: 2, 1212 | name: "Carrots", 1213 | quantity: 100, 1214 | unit: "grams", 1215 | category: "vegetables", 1216 | key: 3, 1217 | }, 1218 | { 1219 | id: 3, 1220 | name: "Spinach", 1221 | quantity: 1, 1222 | unit: "bunch", 1223 | category: "vegetables", 1224 | key: 2, 1225 | }, 1226 | { 1227 | id: 4, 1228 | name: "Milk", 1229 | quantity: 10, 1230 | unit: "liter", 1231 | category: "dairy", 1232 | key: 1, 1233 | }, 1234 | { 1235 | id: 5, 1236 | name: "Cheese", 1237 | quantity: 200, 1238 | unit: "grams", 1239 | category: "dairy", 1240 | key: 0, 1241 | }, 1242 | ]}' 1243 | groupBy="category" groupsInitiallyExpanded="false" width="$space-48"> 1244 | <property name="groupHeaderTemplate"> 1245 | <Stack> 1246 | <Text variant="subtitle" value="{$group.key}" /> 1247 | </Stack> 1248 | </property> 1249 | </List> 1250 | <List data='{[ 1251 | { 1252 | id: 0, 1253 | name: "Apples", 1254 | quantity: 5, 1255 | unit: "pieces", 1256 | category: "fruits", 1257 | key: 5, 1258 | }, 1259 | { 1260 | id: 1, 1261 | name: "Bananas", 1262 | quantity: 6, 1263 | unit: "pieces", 1264 | category: "fruits", 1265 | key: 4, 1266 | }, 1267 | { 1268 | id: 2, 1269 | name: "Carrots", 1270 | quantity: 100, 1271 | unit: "grams", 1272 | category: "vegetables", 1273 | key: 3, 1274 | }, 1275 | { 1276 | id: 3, 1277 | name: "Spinach", 1278 | quantity: 1, 1279 | unit: "bunch", 1280 | category: "vegetables", 1281 | key: 2, 1282 | }, 1283 | { 1284 | id: 4, 1285 | name: "Milk", 1286 | quantity: 10, 1287 | unit: "liter", 1288 | category: "dairy", 1289 | key: 1, 1290 | }, 1291 | { 1292 | id: 5, 1293 | name: "Cheese", 1294 | quantity: 200, 1295 | unit: "grams", 1296 | category: "dairy", 1297 | key: 0, 1298 | }, 1299 | ]}' 1300 | groupBy="category" groupsInitiallyExpanded="true" width="$space-48"> 1301 | <property name="groupHeaderTemplate"> 1302 | <Stack> 1303 | <Text variant="subtitle" value="{$group.key}" /> 1304 | </Stack> 1305 | </property> 1306 | </List> 1307 | </HStack> 1308 | </App> 1309 | ``` 1310 | 1311 | %-PROP-END 1312 | 1313 | %-PROP-START hideEmptyGroups 1314 | 1315 | Note how the `meats` category is not displayed in the right `List`: 1316 | 1317 | ```xmlui {7, 19} 1318 | <App> 1319 | <HStack gap="$space-2"> 1320 | <List 1321 | data="{[...]}" 1322 | defaultGroups="{['meats']}" 1323 | groupBy="category" 1324 | hideEmptyGroups="false" 1325 | width="$space-48"> 1326 | <property name="groupHeaderTemplate"> 1327 | <Stack> 1328 | <Text variant="subtitle" value="{$group.key}" /> 1329 | </Stack> 1330 | </property> 1331 | </List> 1332 | <List 1333 | data="{[...]}" 1334 | defaultGroups="{['meats']}" 1335 | groupBy="category" 1336 | hideEmptyGroups="true" 1337 | width="$space-48"> 1338 | <property name="groupHeaderTemplate"> 1339 | <Stack> 1340 | <Text variant="subtitle" value="{$group.key}" /> 1341 | </Stack> 1342 | </property> 1343 | </List> 1344 | </HStack> 1345 | </App> 1346 | ``` 1347 | 1348 | ```xmlui-pg name="Example: hideEmptyGroups" height="400px" 1349 | <App> 1350 | <HStack gap="$space-2"> 1351 | <List data='{[ 1352 | { 1353 | id: 0, 1354 | name: "Apples", 1355 | quantity: 5, 1356 | unit: "pieces", 1357 | category: "fruits", 1358 | key: 5, 1359 | }, 1360 | { 1361 | id: 1, 1362 | name: "Bananas", 1363 | quantity: 6, 1364 | unit: "pieces", 1365 | category: "fruits", 1366 | key: 4, 1367 | }, 1368 | { 1369 | id: 2, 1370 | name: "Carrots", 1371 | quantity: 100, 1372 | unit: "grams", 1373 | category: "vegetables", 1374 | key: 3, 1375 | }, 1376 | { 1377 | id: 3, 1378 | name: "Spinach", 1379 | quantity: 1, 1380 | unit: "bunch", 1381 | category: "vegetables", 1382 | key: 2, 1383 | }, 1384 | { 1385 | id: 4, 1386 | name: "Milk", 1387 | quantity: 10, 1388 | unit: "liter", 1389 | category: "dairy", 1390 | key: 1, 1391 | }, 1392 | { 1393 | id: 5, 1394 | name: "Cheese", 1395 | quantity: 200, 1396 | unit: "grams", 1397 | category: "dairy", 1398 | key: 0, 1399 | }, 1400 | ]}' 1401 | defaultGroups="{['meats']}" groupBy="category" hideEmptyGroups="false" width="$space-48"> 1402 | <property name="groupHeaderTemplate"> 1403 | <Stack> 1404 | <Text variant="subtitle" value="{$group.key}" /> 1405 | </Stack> 1406 | </property> 1407 | </List> 1408 | <List data='{[ 1409 | { 1410 | id: 0, 1411 | name: "Apples", 1412 | quantity: 5, 1413 | unit: "pieces", 1414 | category: "fruits", 1415 | key: 5, 1416 | }, 1417 | { 1418 | id: 1, 1419 | name: "Bananas", 1420 | quantity: 6, 1421 | unit: "pieces", 1422 | category: "fruits", 1423 | key: 4, 1424 | }, 1425 | { 1426 | id: 2, 1427 | name: "Carrots", 1428 | quantity: 100, 1429 | unit: "grams", 1430 | category: "vegetables", 1431 | key: 3, 1432 | }, 1433 | { 1434 | id: 3, 1435 | name: "Spinach", 1436 | quantity: 1, 1437 | unit: "bunch", 1438 | category: "vegetables", 1439 | key: 2, 1440 | }, 1441 | { 1442 | id: 4, 1443 | name: "Milk", 1444 | quantity: 10, 1445 | unit: "liter", 1446 | category: "dairy", 1447 | key: 1, 1448 | }, 1449 | { 1450 | id: 5, 1451 | name: "Cheese", 1452 | quantity: 200, 1453 | unit: "grams", 1454 | category: "dairy", 1455 | key: 0, 1456 | }, 1457 | ]}' 1458 | defaultGroups="{['meats']}" groupBy="category" hideEmptyGroups="true" width="$space-48"> 1459 | <property name="groupHeaderTemplate"> 1460 | <Stack> 1461 | <Text variant="subtitle" value="{$group.key}" /> 1462 | </Stack> 1463 | </property> 1464 | </List> 1465 | </HStack> 1466 | </App> 1467 | ``` 1468 | 1469 | %-PROP-END 1470 | 1471 | %-STYLE-START 1472 | 1473 | `List` is a layout container; its purpose is to render nested child components. 1474 | `List` has no theme variables to change its visual appearance. 1475 | 1476 | %-STYLE-END 1477 | 1478 | %-API-START scrollToBottom 1479 | 1480 | The following example demonstrates `scrollToBottom` and all the other scroll methods: 1481 | 1482 | ```xmlui-pg copy display name="Example: data API Call" height="400px" 1483 | <App layout="condensed-sticky"> 1484 | <AppHeader> 1485 | <HStack> 1486 | <Button onClick="myList.scrollToBottom()">Scroll to Bottom</Button> 1487 | <Button onClick="myList.scrollToTop()">Scroll to Top</Button> 1488 | <Button onClick="myList.scrollToIndex(25)">Scroll to #25</Button> 1489 | <Button onClick="myList.scrollToId('item-40')">Scroll to ID 'item-40'</Button> 1490 | </HStack> 1491 | </AppHeader> 1492 | <List 1493 | id="myList" 1494 | data="{ 1495 | Array.from({ length: 100 }) 1496 | .map((_, i) => ({id: 'item-' + i, value: 'Item #' + i})) 1497 | }"> 1498 | <property name="itemTemplate"> 1499 | <Card> 1500 | <Text value="{$item.value}" /> 1501 | </Card> 1502 | </property> 1503 | </List> 1504 | </App> 1505 | ``` 1506 | 1507 | %-API-END 1508 | 1509 | %-API-START scrollToTop 1510 | 1511 | See the [`scrollToBottom`](#scrolltobottom) example. 1512 | 1513 | %-API-END 1514 | 1515 | %-API-START scrollToIndex 1516 | 1517 | See the [`scrollToBottom`](#scrolltobottom) example. 1518 | 1519 | %-API-END 1520 | 1521 | %-API-START scrollToId 1522 | 1523 | See the [`scrollToBottom`](#scrolltobottom) example. 1524 | 1525 | %-API-END ``` -------------------------------------------------------------------------------- /xmlui/src/components/Slider/Slider.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Testing Notes: the Driver needs to account for the correct positioning of the indicators on the slider 3 | */ 4 | 5 | import { validationStatusValues } from "../abstractions"; 6 | import { getBounds, SKIP_REASON } from "../../testing/component-test-helpers"; 7 | import { expect, test } from "../../testing/fixtures"; 8 | 9 | // ============================================================================= 10 | // BASIC FUNCTIONALITY TESTS 11 | // ============================================================================= 12 | 13 | test.describe("Basic Functionality", () => { 14 | test("component renders", async ({ initTestBed, page }) => { 15 | await initTestBed(`<Slider />`); 16 | await expect(page.getByRole("slider")).toBeVisible(); 17 | }); 18 | 19 | test("component renders with label", async ({ initTestBed, page }) => { 20 | await initTestBed(`<Slider label="Volume" />`); 21 | await expect(page.getByRole("slider")).toBeVisible(); 22 | await expect(page.getByText("Volume")).toBeVisible(); 23 | }); 24 | 25 | test("sets initialValue of field", async ({ initTestBed, page }) => { 26 | await initTestBed(` 27 | <Fragment> 28 | <Slider id="slider" initialValue="5" /> 29 | <Text testId="slider-value" value="{slider.value}" /> 30 | </Fragment>`); 31 | await expect(page.getByTestId("slider-value")).toHaveText("5"); 32 | }); 33 | 34 | test("accepts empty as initialValue", async ({ initTestBed, page }) => { 35 | await initTestBed(` 36 | <Fragment> 37 | <Slider id="slider" initialValue="" /> 38 | <Text testId="slider-value" value="{slider.value}" /> 39 | </Fragment>`); 40 | await expect(page.getByTestId("slider-value")).toHaveText("0"); 41 | }); 42 | 43 | [ 44 | { label: "int", value: 5, expected: "5" }, 45 | { label: "float", value: 5.5, expected: "5.5" }, 46 | ].forEach(({ label, value, expected }) => { 47 | test(`handles ${label} correctly`, async ({ initTestBed, page }) => { 48 | await initTestBed(` 49 | <Fragment> 50 | <Slider id="slider" initialValue="${JSON.stringify(value)}" /> 51 | <Text testId="slider-value" value="{slider.value}" /> 52 | </Fragment>`); 53 | await expect(page.getByTestId("slider-value")).toHaveText(expected); 54 | }); 55 | }); 56 | 57 | [ 58 | { label: "string that resolves to int", value: "5", expected: "5" }, 59 | { label: "string that resolves to float", value: "5.5", expected: "5.5" }, 60 | ].forEach(({ label, value, expected }) => { 61 | test(`handles ${label} correctly`, async ({ initTestBed, page }) => { 62 | await initTestBed(` 63 | <Fragment> 64 | <Slider id="slider" initialValue="${value}" /> 65 | <Text testId="slider-value" value="{slider.value}" /> 66 | </Fragment>`); 67 | await expect(page.getByTestId("slider-value")).toHaveText(expected); 68 | }); 69 | }); 70 | 71 | [ 72 | { label: "NaN", value: NaN }, 73 | { label: "null", value: null }, 74 | { label: "undefined", value: undefined }, 75 | { label: "empty string", value: "" }, 76 | { label: "string not resolving to number", value: "abc" }, 77 | ].forEach(({ label, value }) => { 78 | test(`handles ${label} gracefully`, async ({ initTestBed, page }) => { 79 | await initTestBed(` 80 | <Fragment> 81 | <Slider id="slider" initialValue="${value}" /> 82 | <Text testId="slider-value" value="{slider.value}" /> 83 | </Fragment>`); 84 | await expect(page.getByTestId("slider-value")).toHaveText("0"); 85 | }); 86 | }); 87 | 88 | test("minValue sets the lower bound", async ({ initTestBed, page }) => { 89 | await initTestBed(` 90 | <Fragment> 91 | <Slider id="slider" minValue="5" /> 92 | <Text testId="slider-value" value="{slider.value}" /> 93 | </Fragment>`); 94 | const slider = page.getByRole("slider"); 95 | await expect(slider).toHaveAttribute("aria-valuemin", "5"); 96 | }); 97 | 98 | test("value cannot be lower than minValue", async ({ initTestBed, page }) => { 99 | await initTestBed(` 100 | <Fragment> 101 | <Slider id="slider" minValue="20" maxValue="30" initialValue="10" /> 102 | <Text testId="slider-value" value="{slider.value}" /> 103 | </Fragment>`); 104 | await expect(page.getByTestId("slider-value")).toHaveText("20"); 105 | }); 106 | 107 | test("maxValue sets the upper bound", async ({ initTestBed, page }) => { 108 | await initTestBed(`<Slider maxValue="50" />`); 109 | const slider = page.getByRole("slider"); 110 | await expect(slider).toHaveAttribute("aria-valuemax", "50"); 111 | }); 112 | 113 | test("value cannot be larger than maxValue", async ({ initTestBed, page }) => { 114 | await initTestBed(` 115 | <Fragment> 116 | <Slider id="slider" maxValue="30" initialValue="40" /> 117 | <Text testId="slider-value" value="{slider.value}" /> 118 | </Fragment>`); 119 | await expect(page.getByTestId("slider-value")).toHaveText("30"); 120 | }); 121 | 122 | test("handles invalid minValue/maxValue gracefully", async ({ initTestBed, page }) => { 123 | await initTestBed(`<Slider minValue="invalid" maxValue="invalid" />`); 124 | const slider = page.getByRole("slider"); 125 | await expect(slider).toHaveAttribute("aria-valuemin", "0"); 126 | await expect(slider).toHaveAttribute("aria-valuemax", "10"); 127 | }); 128 | 129 | test("step defines increment value", async ({ initTestBed, page }) => { 130 | await initTestBed(` 131 | <Fragment> 132 | <Slider id="slider" step="2" initialValue="0" /> 133 | <Text testId="slider-value" value="{slider.value}" /> 134 | </Fragment>`); 135 | const slider = page.getByRole("slider"); 136 | await slider.press("ArrowRight"); 137 | await expect(page.getByTestId("slider-value")).toHaveText("2"); 138 | }); 139 | 140 | test("handles fractional step values", async ({ initTestBed, page }) => { 141 | await initTestBed(` 142 | <Fragment> 143 | <Slider id="slider" step="0.1" initialValue="0" /> 144 | <Text testId="slider-value" value="{slider.value}" /> 145 | </Fragment>`); 146 | const slider = page.getByRole("slider"); 147 | await slider.press("ArrowRight"); 148 | await expect(page.getByTestId("slider-value")).toHaveText("0.1"); 149 | }); 150 | 151 | test("component handles multiple thumbs", async ({ initTestBed, page }) => { 152 | await initTestBed(`<Slider initialValue="{[2, 4]}" />`); 153 | const thumbs = page.getByRole("slider"); 154 | await expect(thumbs).toHaveCount(2); 155 | }); 156 | 157 | test("all thumbs are interactable via mouse", async ({ 158 | initTestBed, 159 | createSliderDriver, 160 | page, 161 | }) => { 162 | await initTestBed(` 163 | <Fragment> 164 | <Slider id="slider" initialValue="{[2, 4]}" minValue="0" maxValue="10" /> 165 | <Text testId="sliderValue0">{slider.value[0]}</Text> 166 | <Text testId="sliderValue1">{slider.value[1]}</Text> 167 | </Fragment> 168 | `); 169 | const driver = await createSliderDriver("slider"); 170 | await driver.dragThumbByMouse("start", 0); 171 | await driver.dragThumbByMouse("end", 1); 172 | 173 | await expect(page.getByTestId("sliderValue0")).toHaveText("0"); 174 | await expect(page.getByTestId("sliderValue1")).toHaveText("10"); 175 | }); 176 | 177 | test("all thumbs are interactable via keyboard", async ({ initTestBed, createSliderDriver, page }) => { 178 | await initTestBed(` 179 | <Fragment> 180 | <Slider id="slider" initialValue="{[2, 4]}" minValue="0" maxValue="10" /> 181 | <Text testId="sliderValue0">{slider.value[0]}</Text> 182 | <Text testId="sliderValue1">{slider.value[1]}</Text> 183 | </Fragment> 184 | `); 185 | const driver = await createSliderDriver("slider"); 186 | await driver.stepThumbByKeyboard("ArrowLeft", 0); 187 | await driver.stepThumbByKeyboard("ArrowRight", 1); 188 | await expect(page.getByTestId("sliderValue0")).toHaveText("1"); 189 | await expect(page.getByTestId("sliderValue1")).toHaveText("5"); 190 | }); 191 | 192 | test("minStepsBetweenThumbs maintains thumb separation", async ({ initTestBed, createSliderDriver, page }) => { 193 | await initTestBed(` 194 | <Fragment> 195 | <Slider id="slider" initialValue="{[0, 5]}" minStepsBetweenThumbs="3" minValue="0" maxValue="10" /> 196 | <Text testId="sliderValue1">{slider.value[1]}</Text> 197 | </Fragment> 198 | `); 199 | const driver = await createSliderDriver("slider"); 200 | await driver.stepThumbByKeyboard("ArrowLeft", 1, 3); // Try to move left by 3 steps 201 | await expect(page.getByTestId("sliderValue1")).toHaveText("3"); 202 | }); 203 | 204 | test("enabled=false disables control", async ({ initTestBed, page }) => { 205 | await initTestBed(`<Slider enabled="false" />`); 206 | await expect(page.getByRole("slider")).toBeDisabled(); 207 | }); 208 | 209 | test("readOnly prevents interaction", async ({ initTestBed, page, createSliderDriver }) => { 210 | await initTestBed(` 211 | <Fragment> 212 | <Slider id="mySlider" readOnly="true" /> 213 | <Text testId="slider-value" value="{mySlider.value}" /> 214 | </Fragment>`); 215 | const driver = await createSliderDriver("mySlider"); 216 | await driver.dragThumbByMouse("end"); 217 | await expect(page.getByTestId("slider-value")).toHaveText("0"); // Value should remain unchanged 218 | }); 219 | 220 | test.fixme( 221 | "autoFocus focuses slider on mount", 222 | SKIP_REASON.XMLUI_BUG("autoFocus does not seem to work with radix-ui, need to double-check"), 223 | async ({ initTestBed, page }) => { 224 | await initTestBed(`<Slider autoFocus="true" />`); 225 | await expect(page.getByRole("slider")).toBeFocused(); 226 | }, 227 | ); 228 | 229 | test("required shows visual indicator", async ({ initTestBed, page }) => { 230 | await initTestBed(`<Slider required="true" label="Required slider" />`); 231 | await expect(page.getByText("Required slider")).toContainText("*"); 232 | }); 233 | 234 | test("showValues=true displays current value", async ({ initTestBed, page }) => { 235 | await initTestBed(`<Slider showValues="true" initialValue="10" />`); 236 | await page.getByRole("slider").hover(); 237 | await expect(page.getByText("10")).toBeVisible(); 238 | }); 239 | 240 | test("showValues=false hides current value", async ({ initTestBed, page }) => { 241 | await initTestBed(`<Slider showValues="false" initialValue="10" />`); 242 | await page.getByRole("slider").hover(); 243 | await expect(page.getByText("10")).not.toBeVisible(); 244 | }); 245 | 246 | test("valueFormat customizes value display", async ({ initTestBed, page }) => { 247 | await initTestBed( 248 | `<Slider showValues="true" initialValue="10" valueFormat="{(v) => v + '%'}" />`, 249 | ); 250 | await page.getByRole("slider").hover(); 251 | await expect(page.getByText("10%")).toBeVisible(); 252 | }); 253 | 254 | test("handles array initialValue for range", async ({ initTestBed, page }) => { 255 | await initTestBed(`<Slider initialValue="{[2, 6]}" />`); 256 | const sliders = page.getByRole("slider"); 257 | 258 | await sliders.first().hover(); 259 | await expect(page.getByText("2")).toBeVisible(); 260 | await expect(page.getByText("6")).toBeVisible(); 261 | }); 262 | 263 | test("maintains thumb order in range mode", async ({ initTestBed, page }) => { 264 | await initTestBed(`<Slider initialValue="{[6, 2]}" />`); 265 | const sliders = page.getByRole("slider"); 266 | // Should auto-correct to [2, 6] 267 | await sliders.first().hover(); 268 | await expect(page.getByText("2")).toBeVisible(); 269 | await expect(page.getByText("6")).toBeVisible(); 270 | }); 271 | }); 272 | 273 | // ============================================================================= 274 | // ACCESSIBILITY TESTS 275 | // ============================================================================= 276 | 277 | test.describe("Accessibility", () => { 278 | test("has correct ARIA role", async ({ initTestBed, page }) => { 279 | await initTestBed(`<Slider />`); 280 | await expect(page.getByRole("slider")).toBeVisible(); 281 | }); 282 | test("label is properly associated", async ({ initTestBed, page }) => { 283 | await initTestBed(`<Slider label="Volume Control" />`); 284 | await page.getByText("Volume Control").click(); 285 | await expect(page.getByRole("slider")).toBeFocused(); 286 | }); 287 | 288 | test("supports keyboard navigation", async ({ initTestBed, page }) => { 289 | await initTestBed(` 290 | <Fragment> 291 | <Slider id="mySlider" initialValue="5" /> 292 | <Text testId="slider-value">{mySlider.value}</Text> 293 | </Fragment>`); 294 | const slider = page.getByRole("slider"); 295 | await slider.focus(); 296 | 297 | await slider.press("ArrowRight"); 298 | await expect(page.getByTestId("slider-value")).toHaveText("6"); 299 | 300 | await slider.press("ArrowLeft"); 301 | await expect(page.getByTestId("slider-value")).toHaveText("5"); 302 | }); 303 | 304 | test("supports Home/End key navigation", async ({ initTestBed, page }) => { 305 | await initTestBed(` 306 | <Fragment> 307 | <Slider id="mySlider" minValue="0" maxValue="100" initialValue="50" /> 308 | <Text testId="slider-value">{mySlider.value}</Text> 309 | </Fragment>`); 310 | const slider = page.getByRole("slider"); 311 | await slider.focus(); 312 | 313 | await slider.press("Home"); 314 | await expect(page.getByTestId("slider-value")).toHaveText("0"); 315 | 316 | await slider.press("End"); 317 | await expect(page.getByTestId("slider-value")).toHaveText("100"); 318 | }); 319 | 320 | test("disabled slider has proper ARIA attributes", async ({ initTestBed, page }) => { 321 | await initTestBed(`<Slider enabled="false" />`); 322 | await expect(page.locator("span").first()).toHaveAttribute("aria-disabled", "true"); 323 | }); 324 | 325 | test("required slider has proper ARIA attributes", async ({ initTestBed, page }) => { 326 | await initTestBed(`<Slider required="true" />`); 327 | await expect(page.getByRole("slider")).toHaveAttribute("aria-required", "true"); 328 | }); 329 | 330 | test("range slider has multiple slider roles", async ({ initTestBed, page }) => { 331 | await initTestBed(`<Slider initialValue="{[3, 5]}" />`); 332 | const sliders = page.getByRole("slider"); 333 | await expect(sliders).toHaveCount(2); 334 | }); 335 | }); 336 | 337 | // ============================================================================= 338 | // LABEL POSITIONING TESTS 339 | // ============================================================================= 340 | 341 | test.describe("Label", () => { 342 | test("labelPosition=top positions label above slider", async ({ initTestBed, page }) => { 343 | await initTestBed(`<Slider label="test" labelPosition="top" />`); 344 | 345 | const { top: sliderTop } = await getBounds(page.getByRole("slider")); 346 | const { bottom: labelBottom } = await getBounds(page.getByText("test")); 347 | 348 | expect(labelBottom).toBeLessThan(sliderTop); 349 | }); 350 | 351 | test("labelPosition=start positions label before slider", async ({ initTestBed, page }) => { 352 | await initTestBed(`<Slider label="test" labelPosition="start" />`); 353 | 354 | const { left: sliderLeft } = await getBounds(page.getByRole("slider")); 355 | const { right: labelRight } = await getBounds(page.getByText("test")); 356 | 357 | expect(labelRight).toBeLessThan(sliderLeft); 358 | }); 359 | 360 | test("labelWidth applies custom label width", async ({ initTestBed, page }) => { 361 | const expected = 150; 362 | await initTestBed(`<Slider label="test label" labelWidth="${expected}px" />`); 363 | const { width } = await getBounds(page.getByText("test label")); 364 | expect(width).toEqual(expected); 365 | }); 366 | 367 | test("labelBreak enables label line breaks", async ({ initTestBed, page }) => { 368 | const labelText = "Very long label text that should break"; 369 | await initTestBed(`<Slider label="${labelText}" labelWidth="100px" labelBreak="true" />`); 370 | const label = page.getByText(labelText); 371 | const { height } = await getBounds(label); 372 | expect(height).toBeGreaterThan(20); // Assumes multi-line height 373 | }); 374 | 375 | test("handles invalid labelPosition gracefully", async ({ initTestBed, page }) => { 376 | await initTestBed(`<Slider labelPosition="invalid" label="test" />`); 377 | await expect(page.getByRole("slider")).toBeVisible(); 378 | await expect(page.getByText("test")).toBeVisible(); 379 | }); 380 | }); 381 | 382 | // ============================================================================= 383 | // EVENT HANDLING TESTS 384 | // ============================================================================= 385 | 386 | test.describe("Event Handling", () => { 387 | test("didChange event fires on value change", async ({ initTestBed, page }) => { 388 | const { testStateDriver } = await initTestBed(` 389 | <Slider onDidChange="testState = 'changed'" initialValue="0" /> 390 | `); 391 | const slider = page.getByRole("slider"); 392 | await slider.focus(); 393 | await slider.press("ArrowRight"); 394 | await expect.poll(testStateDriver.testState).toEqual("changed"); 395 | }); 396 | 397 | test("didChange event passes new value", async ({ initTestBed, page }) => { 398 | const { testStateDriver } = await initTestBed(` 399 | <Slider onDidChange="arg => testState = arg" initialValue="1" /> 400 | `); 401 | const slider = page.getByRole("slider"); 402 | await slider.focus(); 403 | await slider.press("ArrowRight"); 404 | await expect.poll(testStateDriver.testState).toEqual(2); 405 | }); 406 | 407 | test("gotFocus event fires on focus", async ({ initTestBed, page }) => { 408 | const { testStateDriver } = await initTestBed(` 409 | <Slider onGotFocus="testState = 'focused'" /> 410 | `); 411 | await page.getByRole("slider").focus(); 412 | await expect.poll(testStateDriver.testState).toEqual("focused"); 413 | }); 414 | 415 | test("gotFocus event fires on label click", async ({ initTestBed, page }) => { 416 | const { testStateDriver } = await initTestBed(` 417 | <Slider label="Volume" onGotFocus="testState = 'focused'" /> 418 | `); 419 | await page.getByText("Volume").click(); 420 | await expect.poll(testStateDriver.testState).toEqual("focused"); 421 | }); 422 | 423 | test("lostFocus event fires on blur", async ({ initTestBed, page }) => { 424 | const { testStateDriver } = await initTestBed(` 425 | <Slider onLostFocus="testState = 'blurred'" /> 426 | `); 427 | const slider = page.getByRole("slider"); 428 | await slider.focus(); 429 | await slider.blur(); 430 | await expect.poll(testStateDriver.testState).toEqual("blurred"); 431 | }); 432 | 433 | test("events do not fire when disabled", async ({ initTestBed, page }) => { 434 | const { testStateDriver } = await initTestBed(` 435 | <Slider enabled="false" onDidChange="testState = 'changed'" onGotFocus="testState = 'focused'" /> 436 | `); 437 | const slider = page.getByRole("slider"); 438 | await slider.focus(); 439 | await slider.press("ArrowRight"); 440 | await expect.poll(testStateDriver.testState).toEqual(null); 441 | }); 442 | }); 443 | 444 | // ============================================================================= 445 | // API TESTS 446 | // ============================================================================= 447 | 448 | test.describe("Api", () => { 449 | test("value API returns current state", async ({ initTestBed, page }) => { 450 | await initTestBed(` 451 | <Fragment> 452 | <Slider id="mySlider" initialValue="5" /> 453 | <Text testId="value">{mySlider.value}</Text> 454 | </Fragment> 455 | `); 456 | await expect(page.getByTestId("value")).toHaveText("5"); 457 | }); 458 | 459 | test("value API returns state after change", async ({ initTestBed, page }) => { 460 | await initTestBed(` 461 | <Fragment> 462 | <Slider id="mySlider" initialValue="1" /> 463 | <Text testId="value">{mySlider.value}</Text> 464 | </Fragment> 465 | `); 466 | const slider = page.getByRole("slider"); 467 | await slider.focus(); 468 | await slider.press("ArrowRight"); 469 | await expect(page.getByTestId("value")).toHaveText("2"); 470 | }); 471 | 472 | test("setValue API updates state", async ({ initTestBed, page }) => { 473 | await initTestBed(` 474 | <Fragment> 475 | <Slider id="mySlider" /> 476 | <Button testId="setBtn" onClick="mySlider.setValue(5)" label="{mySlider.value}" /> 477 | </Fragment> 478 | `); 479 | await page.getByTestId("setBtn").click(); 480 | await expect(page.getByTestId("setBtn")).toHaveText("5"); 481 | }); 482 | 483 | test("setValue API triggers events", async ({ initTestBed, page }) => { 484 | const { testStateDriver } = await initTestBed(` 485 | <Fragment> 486 | <Slider id="mySlider" onDidChange="testState = 'api-changed'" /> 487 | <Button testId="setBtn" onClick="mySlider.setValue(75)" /> 488 | </Fragment> 489 | `); 490 | await page.getByTestId("setBtn").click(); 491 | await expect.poll(testStateDriver.testState).toEqual("api-changed"); 492 | }); 493 | 494 | test("focus API focuses the slider", async ({ initTestBed, page }) => { 495 | await initTestBed(` 496 | <Fragment> 497 | <Slider id="mySlider" /> 498 | <Button testId="focusBtn" onClick="mySlider.focus()" /> 499 | </Fragment> 500 | `); 501 | const slider = page.getByRole("slider"); 502 | await expect(slider).not.toBeFocused(); 503 | 504 | await page.getByTestId("focusBtn").click(); 505 | await expect(slider).toBeFocused(); 506 | }); 507 | 508 | test("focus API does nothing when disabled", async ({ initTestBed, page }) => { 509 | await initTestBed(` 510 | <Fragment> 511 | <Slider id="mySlider" enabled="false" /> 512 | <Button testId="focusBtn" onClick="mySlider.focus()" /> 513 | </Fragment> 514 | `); 515 | await page.getByTestId("focusBtn").click(); 516 | await expect(page.getByRole("slider")).not.toBeFocused(); 517 | }); 518 | 519 | test("setValue does not update when disabled", async ({ initTestBed, page }) => { 520 | await initTestBed(` 521 | <Fragment> 522 | <Slider id="mySlider" enabled="false" initialValue="5" /> 523 | <Button testId="setBtn" onClick="mySlider.setValue(10)" label="{mySlider.value}" /> 524 | </Fragment> 525 | `); 526 | await page.getByTestId("setBtn").click(); 527 | await expect(page.getByTestId("setBtn")).toHaveText("5"); 528 | }); 529 | 530 | test("setValue handles invalid values gracefully", async ({ initTestBed, page }) => { 531 | await initTestBed(` 532 | <Fragment> 533 | <Slider id="mySlider" initialValue="5" minValue="0" /> 534 | <Button testId="setBtn" onClick="mySlider.setValue('invalid')" label="{mySlider.value}" /> 535 | </Fragment> 536 | `); 537 | await page.getByTestId("setBtn").click(); 538 | await expect(page.getByTestId("setBtn")).toHaveText("0"); 539 | }); 540 | }); 541 | 542 | // ============================================================================= 543 | // THEME VARIABLE TESTS 544 | // ============================================================================= 545 | 546 | test.describe("Theme Variables", () => { 547 | test("backgroundColor-track applies correctly", async ({ initTestBed, page }) => { 548 | await initTestBed(`<Slider testId="slider" />`, { 549 | testThemeVars: { 550 | "backgroundColor-track-Slider": "rgb(255, 0, 0)", 551 | }, 552 | }); 553 | const track = page.locator("[data-track]"); 554 | await expect(track).toHaveCSS("background-color", "rgb(255, 0, 0)"); 555 | }); 556 | 557 | test("backgroundColor-range applies correctly", async ({ initTestBed, page }) => { 558 | await initTestBed(`<Slider testId="slider" initialValue="5" />`, { 559 | testThemeVars: { 560 | "backgroundColor-range-Slider": "rgb(0, 255, 0)", 561 | }, 562 | }); 563 | const range = page.locator("[data-range]"); 564 | await expect(range).toHaveCSS("background-color", "rgb(0, 255, 0)"); 565 | }); 566 | 567 | test("backgroundColor-thumb applies correctly", async ({ initTestBed, page }) => { 568 | await initTestBed(`<Slider testId="slider" />`, { 569 | testThemeVars: { 570 | "backgroundColor-thumb-Slider": "rgb(0, 0, 255)", 571 | }, 572 | }); 573 | const thumb = page.getByRole("slider"); 574 | await expect(thumb).toHaveCSS("background-color", "rgb(0, 0, 255)"); 575 | }); 576 | 577 | test("disabled theme variables apply when disabled", async ({ initTestBed, page }) => { 578 | await initTestBed(`<Slider testId="slider" enabled="false" />`, { 579 | testThemeVars: { 580 | "backgroundColor-track-Slider--disabled": "rgb(200, 200, 200)", 581 | }, 582 | }); 583 | const track = page.locator("[data-track]"); 584 | await expect(track).toHaveCSS("background-color", "rgb(200, 200, 200)"); 585 | }); 586 | 587 | test("focus theme variables apply on focus", async ({ initTestBed, page }) => { 588 | await initTestBed(`<Slider testId="slider" />`, { 589 | testThemeVars: { 590 | "boxShadow-thumb-Slider--focus": "rgb(0, 123, 255) 0px 0px 10px 0px", 591 | }, 592 | }); 593 | const slider = page.getByRole("slider"); 594 | await slider.focus(); 595 | await expect(slider).toHaveCSS("box-shadow", "rgb(0, 123, 255) 0px 0px 10px 0px"); 596 | }); 597 | 598 | test("hover theme variables apply on hover", async ({ initTestBed, page }) => { 599 | await initTestBed(`<Slider testId="slider" />`, { 600 | testThemeVars: { 601 | "boxShadow-thumb-Slider--hover": "rgb(0, 0, 0) 0px 0px 5px 0px", 602 | }, 603 | }); 604 | const slider = page.getByRole("slider"); 605 | await slider.hover(); 606 | await expect(slider).toHaveCSS("box-shadow", "rgb(0, 0, 0) 0px 0px 5px 0px"); 607 | }); 608 | }); 609 | 610 | // ============================================================================= 611 | // VALIDATION STATUS TESTS 612 | // ============================================================================= 613 | 614 | test.describe("Validation", () => { 615 | test(`validationStatus=error applies correctly`, async ({ initTestBed, page }) => { 616 | await initTestBed(`<Slider validationStatus="error" />`, { 617 | testThemeVars: { 618 | [`borderColor-Slider-error`]: "rgb(255, 0, 0)", 619 | }, 620 | }); 621 | const sliderTrack = page.locator("[data-track]"); 622 | await expect(sliderTrack).toHaveCSS("border-color", "rgb(255, 0, 0)"); 623 | }); 624 | 625 | // NOTE: warning color is not applied correctly 626 | test.fixme(`validationStatus=warning applies correctly`, async ({ initTestBed, page }) => { 627 | await initTestBed(`<Slider validationStatus="warning" />`, { 628 | testThemeVars: { 629 | [`borderColor-Slider-warning`]: "rgb(255, 165, 0)", 630 | }, 631 | }); 632 | const sliderTrack = page.locator("[data-track]"); 633 | await expect(sliderTrack).toHaveCSS("border-color", "rgb(218, 127, 0)"); 634 | }); 635 | 636 | test(`validationStatus=valid applies correctly`, async ({ initTestBed, page }) => { 637 | await initTestBed(`<Slider validationStatus="valid" />`, { 638 | testThemeVars: { 639 | [`borderColor-Slider-success`]: "rgb(0, 255, 0)", 640 | }, 641 | }); 642 | const sliderTrack = page.locator("[data-track]"); 643 | await expect(sliderTrack).toHaveCSS("border-color", "rgb(0, 255, 0)"); 644 | }); 645 | 646 | test("handles invalid validationStatus gracefully", async ({ initTestBed, page }) => { 647 | await initTestBed(`<Slider validationStatus="invalid" />`); 648 | await expect(page.getByRole("slider")).toBeVisible(); 649 | }); 650 | }); 651 | 652 | // ============================================================================= 653 | // EDGE CASE TESTS 654 | // ============================================================================= 655 | 656 | test.describe("Edge Cases", () => { 657 | test("handles extremely large values", async ({ initTestBed, page }) => { 658 | await initTestBed(` 659 | <Fragment> 660 | <Slider id="mySlider" initialValue="${Number.MAX_SAFE_INTEGER}" maxValue="${Number.MAX_SAFE_INTEGER}" /> 661 | <Text testId="slider-value" value="{mySlider.value}" /> 662 | </Fragment>`); 663 | await expect(page.getByTestId("slider-value")).toHaveText(Number.MAX_SAFE_INTEGER.toString()); 664 | }); 665 | 666 | test("handles negative values", async ({ initTestBed, page }) => { 667 | await initTestBed(` 668 | <Fragment> 669 | <Slider id="mySlider" minValue="-100" maxValue="0" initialValue="-50" /> 670 | <Text testId="slider-value" value="{mySlider.value}" /> 671 | </Fragment>`); 672 | await expect(page.getByTestId("slider-value")).toHaveText("-50"); 673 | }); 674 | 675 | test("handles very small step values", async ({ initTestBed, page }) => { 676 | await initTestBed(` 677 | <Fragment> 678 | <Slider id="mySlider" step="0.000000001" initialValue="0" /> 679 | <Text testId="slider-value" value="{mySlider.value}" /> 680 | </Fragment>`); 681 | const slider = page.getByRole("slider"); 682 | await slider.focus(); 683 | await slider.press("ArrowRight"); 684 | await expect(page.getByTestId("slider-value")).toHaveText((0.000000001).toExponential()); 685 | }); 686 | 687 | test("handles conflicting min/max values", async ({ initTestBed, page }) => { 688 | await initTestBed(`<Slider minValue="100" maxValue="50" />`); 689 | await expect(page.getByRole("slider")).toBeVisible(); 690 | }); 691 | 692 | test("handles zero step value", async ({ initTestBed, page }) => { 693 | await initTestBed(`<Slider step="0" />`); 694 | await expect(page.getByRole("slider")).toBeVisible(); 695 | }); 696 | 697 | test("handles range with identical values", async ({ initTestBed, page }) => { 698 | await initTestBed(`<Slider initialValue="{[50, 50]}" />`); 699 | const sliders = page.getByRole("slider"); 700 | await expect(sliders).toHaveCount(2); 701 | }); 702 | }); 703 | 704 | // ============================================================================= 705 | // VISUAL STATE TESTS 706 | // ============================================================================= 707 | 708 | test("input has correct width in px", async ({ page, initTestBed }) => { 709 | await initTestBed(`<Slider width="200px" testId="test"/>`, {}); 710 | 711 | const input = page.getByTestId("test"); 712 | const { width } = await input.boundingBox(); 713 | expect(width).toBe(200); 714 | }); 715 | 716 | test("input with label has correct width in px", async ({ page, initTestBed }) => { 717 | await initTestBed(`<Slider width="200px" label="test" testId="test"/>`, {}); 718 | 719 | const input = page.getByTestId("test"); 720 | const { width } = await input.boundingBox(); 721 | expect(width).toBe(200); 722 | }); 723 | 724 | test("input has correct width in %", async ({ page, initTestBed }) => { 725 | await page.setViewportSize({ width: 400, height: 300}); 726 | await initTestBed(`<Slider width="50%" testId="test"/>`, {}); 727 | 728 | const input = page.getByTestId("test"); 729 | const { width } = await input.boundingBox(); 730 | expect(width).toBe(200); 731 | }); 732 | 733 | test("input with label has correct width in %", async ({ page, initTestBed }) => { 734 | await page.setViewportSize({ width: 400, height: 300}); 735 | await initTestBed(`<Slider width="50%" label="test" testId="test"/>`, {}); 736 | 737 | const input = page.getByTestId("test"); 738 | const { width } = await input.boundingBox(); 739 | expect(width).toBe(200); 740 | }); ```