From 131f5582fd933dafd465d26c5741a82b568c872b Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Tue, 13 Jan 2026 12:42:36 +0530 Subject: [PATCH 01/39] 662 - content tree init --- .../modus-wc-content-tree.scss | 31 +++ .../modus-wc-content-tree.spec.ts | 0 .../modus-wc-content-tree.stories.ts | 217 ++++++++++++++++++ .../modus-wc-content-tree.tsx | 94 ++++++++ .../modus-wc-content-tree/readme.md | 45 ++++ 5 files changed, 387 insertions(+) create mode 100644 src/components/modus-wc-content-tree/modus-wc-content-tree.scss create mode 100644 src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts create mode 100644 src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts create mode 100644 src/components/modus-wc-content-tree/modus-wc-content-tree.tsx create mode 100644 src/components/modus-wc-content-tree/readme.md diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss new file mode 100644 index 0000000000..3eb0850112 --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -0,0 +1,31 @@ +/** + * This component uses menu items for the tree structure. + * Only add styles here that should not be applied by Tailwind, Daisy, or the theme. + */ + +modus-wc-content-tree { + display: block; + width: 100%; + + .modus-wc-content-tree-search { + margin-bottom: var(--modus-wc-spacing-md, 1rem); + } + + .modus-wc-content-tree-actions { + align-items: center; + border-bottom: 1px solid var(--modus-wc-color-gray-2, #e0e1e9); + display: flex; + gap: var(--modus-wc-spacing-xs, 0.5rem); + margin-bottom: var(--modus-wc-spacing-md, 1rem); + padding-bottom: var(--modus-wc-spacing-sm, 0.75rem); + } + + .modus-wc-content-tree-content { + display: block; + } + + .modus-wc-menu-dropdown { + // border-inline-start: 2px solid var(--modus-wc-color-primary, #0063a3); + margin-inline-start: var(--modus-wc-spacing-md, 1rem); + } +} diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts new file mode 100644 index 0000000000..4eff6c0366 --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts @@ -0,0 +1,217 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import { html } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; + +interface ContentTreeArgs { + 'custom-class'?: string; + 'multi-select'?: boolean; + 'show-search'?: boolean; + 'show-actions'?: boolean; + 'search-placeholder'?: string; +} + +const meta: Meta = { + title: 'Components/Content Tree', + component: 'modus-wc-content-tree', + args: { + 'multi-select': false, + 'show-search': false, + 'show-actions': false, + 'search-placeholder': 'Search...', + }, + argTypes: { + 'multi-select': { + control: { type: 'boolean' }, + }, + 'show-search': { + control: { type: 'boolean' }, + }, + 'show-actions': { + control: { type: 'boolean' }, + }, + 'search-placeholder': { + control: { type: 'text' }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => { + return html` + + + `; + }, +}; + +export const Collapsed: Story = { + render: (args) => { + const multiSelect = args['multi-select']; + return html` + + + + + + + + + + + + + + + + `; + }, +}; + +export const SingleLevel: Story = { + render: (args) => { + const multiSelect = args['multi-select']; + return html` + + + + + + + + + + + + + + + + `; + }, +}; + +export const WithSearchAndActions: Story = { + args: { + 'show-search': true, + 'show-actions': true, + }, + render: (args) => { + const multiSelect = args['multi-select']; + const tree = document.createElement('modus-wc-content-tree'); + tree.showSearch = args['show-search']; + tree.showActions = args['show-actions']; + tree.searchPlaceholder = args['search-placeholder']; + + tree.innerHTML = ` + + + + + + + + + + + + `; + + return tree; + }, +}; diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx new file mode 100644 index 0000000000..39f469bd7f --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx @@ -0,0 +1,94 @@ +import { Component, Element, h, Host, Prop, State } from '@stencil/core'; +import { Attributes, inheritAriaAttributes } from '../utils'; + +/** + * A customizable content tree component used to display hierarchical data in a tree structure. + * Uses menu items to create the tree structure with support for expanding/collapsing nodes and selection. + */ +@Component({ + tag: 'modus-wc-content-tree', + styleUrl: 'modus-wc-content-tree.scss', + shadow: false, +}) +export class ModusWcContentTree { + private inheritedAttributes: Attributes = {}; + + /** Reference to the host element */ + @Element() el!: HTMLElement; + + /** Custom CSS class to apply to the component. */ + @Prop() customClass?: string = ''; + + /** Whether to show the search input. */ + @Prop() showSearch?: boolean = false; + + /** Whether to show the action bar with add, delete, and collapse all buttons. */ + @Prop() showActions?: boolean = false; + + /** Placeholder text for the search input. */ + @Prop() searchPlaceholder?: string = 'Search...'; + + @State() private searchValue: string = ''; + + componentWillLoad() { + this.inheritedAttributes = inheritAriaAttributes(this.el); + } + + render() { + return ( + +
+ {this.showSearch && ( + + )} + + {this.showActions && ( +
+ + + + + + + + + +
+ )} + +
+ +
+
+
+ ); + } +} diff --git a/src/components/modus-wc-content-tree/readme.md b/src/components/modus-wc-content-tree/readme.md new file mode 100644 index 0000000000..b04c69cfba --- /dev/null +++ b/src/components/modus-wc-content-tree/readme.md @@ -0,0 +1,45 @@ +# modus-wc-content-tree + + + + + + +## Overview + +A customizable content tree component used to display hierarchical data in a tree structure. +Uses menu items to create the tree structure with support for expanding/collapsing nodes and selection. + +## Properties + +| Property | Attribute | Description | Type | Default | +| ------------------- | -------------------- | -------------------------------------------------------------------------- | ---------------------- | ------------- | +| `customClass` | `custom-class` | Custom CSS class to apply to the component. | `string \| undefined` | `''` | +| `searchPlaceholder` | `search-placeholder` | Placeholder text for the search input. | `string \| undefined` | `'Search...'` | +| `showActions` | `show-actions` | Whether to show the action bar with add, delete, and collapse all buttons. | `boolean \| undefined` | `false` | +| `showSearch` | `show-search` | Whether to show the search input. | `boolean \| undefined` | `false` | + + +## Dependencies + +### Depends on + +- [modus-wc-text-input](../modus-wc-text-input) +- [modus-wc-button](../modus-wc-button) +- [modus-wc-icon](../modus-wc-icon) + +### Graph +```mermaid +graph TD; + modus-wc-content-tree --> modus-wc-text-input + modus-wc-content-tree --> modus-wc-button + modus-wc-content-tree --> modus-wc-icon + modus-wc-text-input --> modus-wc-input-label + modus-wc-text-input --> modus-wc-input-feedback + modus-wc-input-feedback --> modus-wc-icon + style modus-wc-content-tree fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* From 74327957dab309e72f7c5597ed85a7c564f85b01 Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:43:07 +0530 Subject: [PATCH 02/39] 662 - update layout --- src/components.d.ts | 124 +++++++++++ src/components/modus-wc-button/readme.md | 4 +- .../modus-wc-content-tree.scss | 42 +++- .../modus-wc-content-tree.stories.ts | 49 +---- .../modus-wc-content-tree.tsx | 76 ++++--- .../modus-wc-content-tree/readme.md | 6 +- src/components/modus-wc-icon/readme.md | 2 + src/components/modus-wc-logo/README.md | 12 +- src/components/modus-wc-text-input/readme.md | 2 + src/components/modus-wc-typography/readme.md | 13 ++ src/custom-elements.json | 196 +++++++++++++----- 11 files changed, 377 insertions(+), 149 deletions(-) diff --git a/src/components.d.ts b/src/components.d.ts index 51f6f687ae..e347ebf656 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -10,6 +10,7 @@ import { IBreadcrumb } from "./components/modus-wc-breadcrumbs/modus-wc-breadcru import { ICollapseOptions } from "./components/modus-wc-collapse/modus-wc-collapse"; import { IInputFeedbackLevel } from "./components/modus-wc-input-feedback/modus-wc-input-feedback"; import { LoaderColor, LoaderVariant } from "./components/modus-wc-loader/modus-wc-loader"; +import { LogoName } from "./components/modus-wc-logo/logo-constants"; import { INavbarTextOverrides, INavbarUserCard, INavbarVisibility } from "./components/modus-wc-navbar/modus-wc-navbar"; import { IAriaLabelValues, IPageChange } from "./components/modus-wc-pagination/modus-wc-pagination"; import { IRatingChange, ModusWcRatingVariant } from "./components/modus-wc-rating/modus-wc-rating"; @@ -26,6 +27,7 @@ export { IBreadcrumb } from "./components/modus-wc-breadcrumbs/modus-wc-breadcru export { ICollapseOptions } from "./components/modus-wc-collapse/modus-wc-collapse"; export { IInputFeedbackLevel } from "./components/modus-wc-input-feedback/modus-wc-input-feedback"; export { LoaderColor, LoaderVariant } from "./components/modus-wc-loader/modus-wc-loader"; +export { LogoName } from "./components/modus-wc-logo/logo-constants"; export { INavbarTextOverrides, INavbarUserCard, INavbarVisibility } from "./components/modus-wc-navbar/modus-wc-navbar"; export { IAriaLabelValues, IPageChange } from "./components/modus-wc-pagination/modus-wc-pagination"; export { IRatingChange, ModusWcRatingVariant } from "./components/modus-wc-rating/modus-wc-rating"; @@ -504,6 +506,28 @@ export namespace Components { */ "options"?: ICollapseOptions; } + /** + * A customizable content tree component used to display hierarchical data in a tree structure. + * Uses menu items to create the tree structure with support for expanding/collapsing nodes and selection. + */ + interface ModusWcContentTree { + /** + * Custom CSS class to apply to the component. + */ + "customClass"?: string; + /** + * Placeholder text for the search input. + */ + "searchPlaceholder"?: string; + /** + * Whether to show the action bar with add, delete, and collapse all buttons. + */ + "showActions"?: boolean; + /** + * Whether to show the search input. + */ + "showSearch"?: boolean; + } /** * A customizable date picker component used to create date inputs. * Adheres to WCAG 2.2 standards. @@ -831,6 +855,28 @@ export namespace Components { */ "variant": LoaderVariant; } + /** + * A component for displaying Trimble product logos with support for both fixed and scalable sizing. + * Provides consistent branding across applications with various product logo options. + */ + interface ModusWcLogo { + /** + * The alt text for accessibility. If not provided, defaults to the logo name. + */ + "alt"?: string; + /** + * Custom CSS class to apply to the logo container. + */ + "customClass"?: string; + /** + * Show emblem version (icon only) instead of full logo + */ + "emblem"?: boolean; + /** + * The name of the logo to display. Accepts values like 'trimble', 'viewpoint_field_view', etc. + */ + "name": LogoName; + } /** * A customizable menu component used to display a list of li elements vertically or horizontally. * The component supports a `` for injecting custom li elements inside the ul @@ -2304,6 +2350,16 @@ declare global { prototype: HTMLModusWcCollapseElement; new (): HTMLModusWcCollapseElement; }; + /** + * A customizable content tree component used to display hierarchical data in a tree structure. + * Uses menu items to create the tree structure with support for expanding/collapsing nodes and selection. + */ + interface HTMLModusWcContentTreeElement extends Components.ModusWcContentTree, HTMLStencilElement { + } + var HTMLModusWcContentTreeElement: { + prototype: HTMLModusWcContentTreeElement; + new (): HTMLModusWcContentTreeElement; + }; interface HTMLModusWcDateElementEventMap { "inputBlur": FocusEvent; "inputChange": InputEvent; @@ -2418,6 +2474,16 @@ declare global { prototype: HTMLModusWcLoaderElement; new (): HTMLModusWcLoaderElement; }; + /** + * A component for displaying Trimble product logos with support for both fixed and scalable sizing. + * Provides consistent branding across applications with various product logo options. + */ + interface HTMLModusWcLogoElement extends Components.ModusWcLogo, HTMLStencilElement { + } + var HTMLModusWcLogoElement: { + prototype: HTMLModusWcLogoElement; + new (): HTMLModusWcLogoElement; + }; interface HTMLModusWcMenuElementEventMap { "menuFocusout": FocusEvent; } @@ -2952,6 +3018,7 @@ declare global { "modus-wc-checkbox": HTMLModusWcCheckboxElement; "modus-wc-chip": HTMLModusWcChipElement; "modus-wc-collapse": HTMLModusWcCollapseElement; + "modus-wc-content-tree": HTMLModusWcContentTreeElement; "modus-wc-date": HTMLModusWcDateElement; "modus-wc-divider": HTMLModusWcDividerElement; "modus-wc-dropdown-menu": HTMLModusWcDropdownMenuElement; @@ -2960,6 +3027,7 @@ declare global { "modus-wc-input-feedback": HTMLModusWcInputFeedbackElement; "modus-wc-input-label": HTMLModusWcInputLabelElement; "modus-wc-loader": HTMLModusWcLoaderElement; + "modus-wc-logo": HTMLModusWcLogoElement; "modus-wc-menu": HTMLModusWcMenuElement; "modus-wc-menu-item": HTMLModusWcMenuItemElement; "modus-wc-modal": HTMLModusWcModalElement; @@ -3517,6 +3585,28 @@ declare namespace LocalJSX { */ "options"?: ICollapseOptions; } + /** + * A customizable content tree component used to display hierarchical data in a tree structure. + * Uses menu items to create the tree structure with support for expanding/collapsing nodes and selection. + */ + interface ModusWcContentTree { + /** + * Custom CSS class to apply to the component. + */ + "customClass"?: string; + /** + * Placeholder text for the search input. + */ + "searchPlaceholder"?: string; + /** + * Whether to show the action bar with add, delete, and collapse all buttons. + */ + "showActions"?: boolean; + /** + * Whether to show the search input. + */ + "showSearch"?: boolean; + } /** * A customizable date picker component used to create date inputs. * Adheres to WCAG 2.2 standards. @@ -3868,6 +3958,28 @@ declare namespace LocalJSX { */ "variant"?: LoaderVariant; } + /** + * A component for displaying Trimble product logos with support for both fixed and scalable sizing. + * Provides consistent branding across applications with various product logo options. + */ + interface ModusWcLogo { + /** + * The alt text for accessibility. If not provided, defaults to the logo name. + */ + "alt"?: string; + /** + * Custom CSS class to apply to the logo container. + */ + "customClass"?: string; + /** + * Show emblem version (icon only) instead of full logo + */ + "emblem"?: boolean; + /** + * The name of the logo to display. Accepts values like 'trimble', 'viewpoint_field_view', etc. + */ + "name": LogoName; + } /** * A customizable menu component used to display a list of li elements vertically or horizontally. * The component supports a `` for injecting custom li elements inside the ul @@ -5235,6 +5347,7 @@ declare namespace LocalJSX { "modus-wc-checkbox": ModusWcCheckbox; "modus-wc-chip": ModusWcChip; "modus-wc-collapse": ModusWcCollapse; + "modus-wc-content-tree": ModusWcContentTree; "modus-wc-date": ModusWcDate; "modus-wc-divider": ModusWcDivider; "modus-wc-dropdown-menu": ModusWcDropdownMenu; @@ -5243,6 +5356,7 @@ declare namespace LocalJSX { "modus-wc-input-feedback": ModusWcInputFeedback; "modus-wc-input-label": ModusWcInputLabel; "modus-wc-loader": ModusWcLoader; + "modus-wc-logo": ModusWcLogo; "modus-wc-menu": ModusWcMenu; "modus-wc-menu-item": ModusWcMenuItem; "modus-wc-modal": ModusWcModal; @@ -5333,6 +5447,11 @@ declare module "@stencil/core" { * Do not set */ "modus-wc-collapse": LocalJSX.ModusWcCollapse & JSXBase.HTMLAttributes; + /** + * A customizable content tree component used to display hierarchical data in a tree structure. + * Uses menu items to create the tree structure with support for expanding/collapsing nodes and selection. + */ + "modus-wc-content-tree": LocalJSX.ModusWcContentTree & JSXBase.HTMLAttributes; /** * A customizable date picker component used to create date inputs. * Adheres to WCAG 2.2 standards. @@ -5370,6 +5489,11 @@ declare module "@stencil/core" { * A customizable loader component used to indicate the loading of content */ "modus-wc-loader": LocalJSX.ModusWcLoader & JSXBase.HTMLAttributes; + /** + * A component for displaying Trimble product logos with support for both fixed and scalable sizing. + * Provides consistent branding across applications with various product logo options. + */ + "modus-wc-logo": LocalJSX.ModusWcLogo & JSXBase.HTMLAttributes; /** * A customizable menu component used to display a list of li elements vertically or horizontally. * The component supports a `` for injecting custom li elements inside the ul diff --git a/src/components/modus-wc-button/readme.md b/src/components/modus-wc-button/readme.md index e3e539647f..1b81d545d6 100644 --- a/src/components/modus-wc-button/readme.md +++ b/src/components/modus-wc-button/readme.md @@ -18,7 +18,7 @@ The component supports a `` for injecting content within the button, simil | `disabled` | `disabled` | If true, the button will be disabled. | `boolean \| undefined` | `false` | | `fullWidth` | `full-width` | If true, the button will take the full width of its container. | `boolean \| undefined` | `false` | | `pressed` | `pressed` | If true, the button will be in a pressed state (for toggle buttons). | `boolean \| undefined` | `false` | -| `shape` | `shape` | The shape of the button. | `"circle" \| "rectangle" \| "square"` | `'rectangle'` | +| `shape` | `shape` | The shape of the button. | `"circle" \| "ellipse" \| "rectangle" \| "square"` | `'rectangle'` | | `size` | `size` | The size of the button. | `"lg" \| "md" \| "sm" \| "xs"` | `'md'` | | `type` | `type` | The type of the button. | `"button" \| "reset" \| "submit"` | `'button'` | | `variant` | `variant` | The variant of the button. | `"borderless" \| "filled" \| "outlined"` | `'filled'` | @@ -37,6 +37,7 @@ The component supports a `` for injecting content within the button, simil - [modus-wc-alert](../modus-wc-alert) - [modus-wc-autocomplete](../modus-wc-autocomplete) + - [modus-wc-content-tree](../modus-wc-content-tree) - [modus-wc-date](../modus-wc-date) - [modus-wc-dropdown-menu](../modus-wc-dropdown-menu) - [modus-wc-modal](../modus-wc-modal) @@ -47,6 +48,7 @@ The component supports a `` for injecting content within the button, simil graph TD; modus-wc-alert --> modus-wc-button modus-wc-autocomplete --> modus-wc-button + modus-wc-content-tree --> modus-wc-button modus-wc-date --> modus-wc-button modus-wc-dropdown-menu --> modus-wc-button modus-wc-modal --> modus-wc-button diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index 3eb0850112..02c5ff3922 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -4,28 +4,56 @@ */ modus-wc-content-tree { + background: white; + border: 1px solid var(--modus-wc-color-border-default, #d1d5db); display: block; + padding: var(--modus-wc-spacing-md, 1rem); width: 100%; - .modus-wc-content-tree-search { - margin-bottom: var(--modus-wc-spacing-md, 1rem); - } - .modus-wc-content-tree-actions { align-items: center; - border-bottom: 1px solid var(--modus-wc-color-gray-2, #e0e1e9); display: flex; gap: var(--modus-wc-spacing-xs, 0.5rem); + justify-content: flex-end; margin-bottom: var(--modus-wc-spacing-md, 1rem); padding-bottom: var(--modus-wc-spacing-sm, 0.75rem); } .modus-wc-content-tree-content { - display: block; + align-items: center; + display: flex; + height: 600px; + justify-content: center; + } + + .modus-wc-content-tree-empty { + align-items: center; + display: flex; + flex-direction: column; + gap: var(--modus-wc-spacing-md, 1rem); + height: 100%; + justify-content: center; + text-align: center; + } + + .modus-wc-content-tree-empty-icon { + color: var(--modus-wc-color-gray-6, #6a6e79); + opacity: 0.5; + } + + .modus-wc-content-tree-empty-title { + color: var(--modus-wc-color-gray-8, #464b52); + font-size: var(--modus-wc-font-size-lg, 1.125rem); + font-weight: 600; + margin: 0; + } + + .modus-wc-content-tree-search { + margin-bottom: var(--modus-wc-spacing-md, 1rem); + width: 304px; } .modus-wc-menu-dropdown { - // border-inline-start: 2px solid var(--modus-wc-color-primary, #0063a3); margin-inline-start: var(--modus-wc-spacing-md, 1rem); } } diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts index 4eff6c0366..7e88193a0c 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts @@ -46,6 +46,7 @@ export const Default: Story = { class=${ifDefined(args['custom-class'])} multi-select="true" show-search="true" + show-actions="true" > `; @@ -167,51 +168,3 @@ export const SingleLevel: Story = { `; }, }; - -export const WithSearchAndActions: Story = { - args: { - 'show-search': true, - 'show-actions': true, - }, - render: (args) => { - const multiSelect = args['multi-select']; - const tree = document.createElement('modus-wc-content-tree'); - tree.showSearch = args['show-search']; - tree.showActions = args['show-actions']; - tree.searchPlaceholder = args['search-placeholder']; - - tree.innerHTML = ` - - - - - - - - - - - - `; - - return tree; - }, -}; diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx index 39f469bd7f..b08f316b9c 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx @@ -29,15 +29,31 @@ export class ModusWcContentTree { @Prop() searchPlaceholder?: string = 'Search...'; @State() private searchValue: string = ''; + @State() private hasSlotContent: boolean = true; componentWillLoad() { this.inheritedAttributes = inheritAriaAttributes(this.el); } + componentDidLoad() { + this.checkSlotContent(); + } + + private checkSlotContent() { + // Since shadow: false, check direct children (excluding the wrapper div) + const children = Array.from(this.el.children).filter( + (child) => !child.classList.contains('modus-wc-content-tree-wrapper') + ); + this.hasSlotContent = children.length > 0; + } + render() { return ( -
+
{this.showSearch && ( diff --git a/src/components/modus-wc-content-tree/readme.md b/src/components/modus-wc-content-tree/readme.md index b04c69cfba..6f91a66b07 100644 --- a/src/components/modus-wc-content-tree/readme.md +++ b/src/components/modus-wc-content-tree/readme.md @@ -25,15 +25,17 @@ Uses menu items to create the tree structure with support for expanding/collapsi ### Depends on - [modus-wc-text-input](../modus-wc-text-input) -- [modus-wc-button](../modus-wc-button) - [modus-wc-icon](../modus-wc-icon) +- [modus-wc-typography](../modus-wc-typography) +- [modus-wc-button](../modus-wc-button) ### Graph ```mermaid graph TD; modus-wc-content-tree --> modus-wc-text-input - modus-wc-content-tree --> modus-wc-button modus-wc-content-tree --> modus-wc-icon + modus-wc-content-tree --> modus-wc-typography + modus-wc-content-tree --> modus-wc-button modus-wc-text-input --> modus-wc-input-label modus-wc-text-input --> modus-wc-input-feedback modus-wc-input-feedback --> modus-wc-icon diff --git a/src/components/modus-wc-icon/readme.md b/src/components/modus-wc-icon/readme.md index 12b06921f2..8d9709a927 100644 --- a/src/components/modus-wc-icon/readme.md +++ b/src/components/modus-wc-icon/readme.md @@ -30,6 +30,7 @@ A customizable icon component used to render Modus icons. - [modus-wc-autocomplete](../modus-wc-autocomplete) - [modus-wc-avatar](../modus-wc-avatar) - [modus-wc-collapse](../modus-wc-collapse) + - [modus-wc-content-tree](../modus-wc-content-tree) - [modus-wc-date](../modus-wc-date) - [modus-wc-file-dropzone](../modus-wc-file-dropzone) - [modus-wc-input-feedback](../modus-wc-input-feedback) @@ -43,6 +44,7 @@ graph TD; modus-wc-autocomplete --> modus-wc-icon modus-wc-avatar --> modus-wc-icon modus-wc-collapse --> modus-wc-icon + modus-wc-content-tree --> modus-wc-icon modus-wc-date --> modus-wc-icon modus-wc-file-dropzone --> modus-wc-icon modus-wc-input-feedback --> modus-wc-icon diff --git a/src/components/modus-wc-logo/README.md b/src/components/modus-wc-logo/README.md index 1d08ab34a2..2c1094d4ac 100644 --- a/src/components/modus-wc-logo/README.md +++ b/src/components/modus-wc-logo/README.md @@ -12,12 +12,12 @@ Provides consistent branding across applications with various product logo optio ## Properties -| Property | Attribute | Description | Type | Default | -| ------------------- | -------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -| `alt` | `alt` | The alt text for accessibility. If not provided, defaults to the logo name. | `string \| undefined` | `undefined` | -| `customClass` | `custom-class` | Custom CSS class to apply to the logo container. | `string \| undefined` | `''` | -| `emblem` | `emblem` | Show emblem version (icon only) instead of full logo | `boolean \| undefined` | `false` | -| `name` _(required)_ | `name` | The name of the logo to display. Accepts values like 'trimble', 'viewpoint_field_view', etc. | `"trimble" \| "siteworks" \| "earthworks" \| "worksmanager" \| "connect" \| "unity_construct" \| "trade_servicelive" \| "buildable" \| "livecount" \| "supplier_xchange" \| "app_xchange" \| "trimble_unity" \| "sketchup" \| "pc_miler" \| "copilot" \| "trimble_pay" \| "projectsight" \| "demand_planning" \| "viewpoint" \| "viewpoint_analytics" \| "viewpoint_epayments" \| "viewpoint_estimating" \| "viewpoint_field_management" \| "viewpoint_field_time" \| "viewpoint_financial_controls" \| "viewpoint_hr_management" \| "viewpoint_jobpac_connect" \| "viewpoint_procontractor" \| "viewpoint_spectrum" \| "viewpoint_team" \| "viewpoint_vista" \| "viewpoint_spectrum_service_tech" \| "viewpoint_for_projects" \| "viewpoint_vista_field_service" \| "viewpoint_field_view"` | `undefined` | +| Property | Attribute | Description | Type | Default | +| ------------------- | -------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | +| `alt` | `alt` | The alt text for accessibility. If not provided, defaults to the logo name. | `string \| undefined` | `undefined` | +| `customClass` | `custom-class` | Custom CSS class to apply to the logo container. | `string \| undefined` | `''` | +| `emblem` | `emblem` | Show emblem version (icon only) instead of full logo | `boolean \| undefined` | `false` | +| `name` _(required)_ | `name` | The name of the logo to display. Accepts values like 'trimble', 'viewpoint_field_view', etc. | `"trimble" \| "siteworks" \| "earthworks" \| "financials" \| "worksmanager" \| "connect" \| "unity_construct" \| "trade_servicelive" \| "buildable" \| "livecount" \| "supplier_xchange" \| "app_xchange" \| "trimble_unity" \| "sketchup" \| "pc_miler" \| "copilot" \| "trimble_pay" \| "projectsight" \| "demand_planning" \| "viewpoint" \| "viewpoint_analytics" \| "viewpoint_epayments" \| "viewpoint_estimating" \| "viewpoint_field_management" \| "viewpoint_field_time" \| "viewpoint_financial_controls" \| "viewpoint_hr_management" \| "viewpoint_jobpac_connect" \| "viewpoint_procontractor" \| "viewpoint_spectrum" \| "viewpoint_team" \| "viewpoint_vista" \| "viewpoint_spectrum_service_tech" \| "viewpoint_for_projects" \| "viewpoint_vista_field_service" \| "viewpoint_field_view"` | `undefined` | ---------------------------------------------- diff --git a/src/components/modus-wc-text-input/readme.md b/src/components/modus-wc-text-input/readme.md index 37bcdcff0f..29167e3dab 100644 --- a/src/components/modus-wc-text-input/readme.md +++ b/src/components/modus-wc-text-input/readme.md @@ -54,6 +54,7 @@ A customizable input component used to create text inputs with types. ### Used by - [modus-wc-autocomplete](../modus-wc-autocomplete) + - [modus-wc-content-tree](../modus-wc-content-tree) - [modus-wc-navbar](../modus-wc-navbar) ### Depends on @@ -68,6 +69,7 @@ graph TD; modus-wc-text-input --> modus-wc-input-feedback modus-wc-input-feedback --> modus-wc-icon modus-wc-autocomplete --> modus-wc-text-input + modus-wc-content-tree --> modus-wc-text-input modus-wc-navbar --> modus-wc-text-input style modus-wc-text-input fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/src/components/modus-wc-typography/readme.md b/src/components/modus-wc-typography/readme.md index 43f9d00476..1859e14f68 100644 --- a/src/components/modus-wc-typography/readme.md +++ b/src/components/modus-wc-typography/readme.md @@ -24,6 +24,19 @@ providing your own custom values for the size or weight properties from the avai | `weight` | `weight` | The weight of the text. | `"bold" \| "light" \| "normal" \| "semibold" \| undefined` | `'normal'` | +## Dependencies + +### Used by + + - [modus-wc-content-tree](../modus-wc-content-tree) + +### Graph +```mermaid +graph TD; + modus-wc-content-tree --> modus-wc-typography + style modus-wc-typography fill:#f9f,stroke:#333,stroke-width:4px +``` + ---------------------------------------------- *Built with [StencilJS](https://stenciljs.com/)* diff --git a/src/custom-elements.json b/src/custom-elements.json index d90b7a53fc..3ab6bcc176 100644 --- a/src/custom-elements.json +++ b/src/custom-elements.json @@ -8,7 +8,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable accordion component used for showing and hiding related groups of content.\n\nThe component supports a `` for injecting `` elements. See [Collapse](/docs/components-collapse--docs) docs for additional info.", + "description": "A customizable accordion component used for showing and hiding related groups of content.\r\n\r\nThe component supports a `` for injecting `` elements. See [Collapse](/docs/components-collapse--docs) docs for additional info.", "name": "ModusWcAccordion", "members": [ { @@ -41,7 +41,7 @@ "kind": "field", "name": "expandedChange", "type": { - "text": "EventEmitter<{\n expanded: boolean;\n index: number;\n }>" + "text": "EventEmitter<{\r\n expanded: boolean;\r\n index: number;\r\n }>" }, "description": "When a collapse expanded state is changed, this event outputs the relevant index and state" } @@ -280,7 +280,7 @@ { "name": "props", "type": { - "text": "{\n bordered?: boolean;\n disabled?: boolean;\n readOnly?: boolean;\n size?: ModusSize;\n}" + "text": "{\r\n bordered?: boolean;\r\n disabled?: boolean;\r\n readOnly?: boolean;\r\n size?: ModusSize;\r\n}" } } ] @@ -332,7 +332,7 @@ { "name": "params", "type": { - "text": "{\n initialNavigation: boolean;\n visibleItems: IAutocompleteItem[];\n onUpdateFocus: (value: string) => void;\n onSetInitialNavigation: (value: boolean) => void;\n}" + "text": "{\r\n initialNavigation: boolean;\r\n visibleItems: IAutocompleteItem[];\r\n onUpdateFocus: (value: string) => void;\r\n onSetInitialNavigation: (value: boolean) => void;\r\n}" } } ] @@ -355,7 +355,7 @@ { "name": "params", "type": { - "text": "{\n multiSelect?: boolean;\n selectionOrder: string[];\n items?: IAutocompleteItem[];\n onChipRemove: (item: IAutocompleteItem) => void;\n }" + "text": "{\r\n multiSelect?: boolean;\r\n selectionOrder: string[];\r\n items?: IAutocompleteItem[];\r\n onChipRemove: (item: IAutocompleteItem) => void;\r\n }" } } ] @@ -365,7 +365,7 @@ "name": "processChipRemoval", "return": { "type": { - "text": "{\n updatedItems: IAutocompleteItem[] | undefined;\n updatedSelectionOrder: string[];\n}" + "text": "{\r\n updatedItems: IAutocompleteItem[] | undefined;\r\n updatedSelectionOrder: string[];\r\n}" } }, "parameters": [ @@ -378,7 +378,7 @@ { "name": "params", "type": { - "text": "{\n disabled?: boolean;\n readOnly?: boolean;\n items?: IAutocompleteItem[];\n selectionOrder: string[];\n }" + "text": "{\r\n disabled?: boolean;\r\n readOnly?: boolean;\r\n items?: IAutocompleteItem[];\r\n selectionOrder: string[];\r\n }" } } ] @@ -388,7 +388,7 @@ "name": "processInputChange", "return": { "type": { - "text": "{\n inputValue: string;\n shouldShowMenu: boolean;\n updatedItems: IAutocompleteItem[] | undefined;\n shouldResetNavigation: boolean;\n}" + "text": "{\r\n inputValue: string;\r\n shouldShowMenu: boolean;\r\n updatedItems: IAutocompleteItem[] | undefined;\r\n shouldResetNavigation: boolean;\r\n}" } }, "parameters": [ @@ -401,7 +401,7 @@ { "name": "params", "type": { - "text": "{\n disabled?: boolean;\n readOnly?: boolean;\n customInputChange?: (value: string) => void;\n showMenuOnFocus?: boolean;\n minChars: number;\n items?: IAutocompleteItem[];\n multiSelect?: boolean;\n debounceMs?: number;\n }" + "text": "{\r\n disabled?: boolean;\r\n readOnly?: boolean;\r\n customInputChange?: (value: string) => void;\r\n showMenuOnFocus?: boolean;\r\n minChars: number;\r\n items?: IAutocompleteItem[];\r\n multiSelect?: boolean;\r\n debounceMs?: number;\r\n }" } } ] @@ -411,7 +411,7 @@ "name": "processItemSelection", "return": { "type": { - "text": "{\n updatedItems: IAutocompleteItem[] | undefined;\n updatedValue: string | undefined;\n updatedSelectionOrder: string[];\n shouldExpandChips: boolean;\n shouldCloseMenu: boolean;\n}" + "text": "{\r\n updatedItems: IAutocompleteItem[] | undefined;\r\n updatedValue: string | undefined;\r\n updatedSelectionOrder: string[];\r\n shouldExpandChips: boolean;\r\n shouldCloseMenu: boolean;\r\n}" } }, "parameters": [ @@ -424,7 +424,7 @@ { "name": "params", "type": { - "text": "{\n disabled?: boolean;\n readOnly?: boolean;\n items?: IAutocompleteItem[];\n multiSelect?: boolean;\n leaveMenuOpen?: boolean;\n selectionOrder: string[];\n maxChips?: number;\n customItemSelect?: (item: IAutocompleteItem) => void;\n }" + "text": "{\r\n disabled?: boolean;\r\n readOnly?: boolean;\r\n items?: IAutocompleteItem[];\r\n multiSelect?: boolean;\r\n leaveMenuOpen?: boolean;\r\n selectionOrder: string[];\r\n maxChips?: number;\r\n customItemSelect?: (item: IAutocompleteItem) => void;\r\n }" } } ] @@ -447,7 +447,7 @@ { "name": "params", "type": { - "text": "{\n disabled?: boolean;\n readOnly?: boolean;\n customKeyDown?: (event: KeyboardEvent) => void;\n }" + "text": "{\r\n disabled?: boolean;\r\n readOnly?: boolean;\r\n customKeyDown?: (event: KeyboardEvent) => void;\r\n }" } } ] @@ -952,7 +952,7 @@ "name": "debounce-ms", "fieldName": "debounceMs", "default": "300", - "description": "The debounce timeout in milliseconds.\nSet to 0 to disable debouncing.", + "description": "The debounce timeout in milliseconds.\r\nSet to 0 to disable debouncing.", "type": { "text": "number" } @@ -1004,7 +1004,7 @@ "name": "items", "fieldName": "items", "default": "[]", - "description": "The items to display in the menu.\nCreating a new array of items will ensure proper component re-render.", + "description": "The items to display in the menu.\r\nCreating a new array of items will ensure proper component re-render.", "type": { "text": "IAutocompleteItem[]" } @@ -1183,7 +1183,7 @@ "type": { "text": "EventEmitter" }, - "description": "Event emitted when the input value changes.\nThis event is debounced based on the debounceMs prop." + "description": "Event emitted when the input value changes.\r\nThis event is debounced based on the debounceMs prop." }, { "kind": "field", @@ -1230,7 +1230,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable avatar component used to create avatars with different images or user initials.\nWhen no image is provided, the component can display initials (up to 3 characters) from the initials prop.\nThe component will extract the first letter of each word in the initials string.", + "description": "A customizable avatar component used to create avatars with different images or user initials.\r\nWhen no image is provided, the component can display initials (up to 3 characters) from the initials prop.\r\nThe component will extract the first letter of each word in the initials string.", "name": "ModusWcAvatar", "members": [ { @@ -1331,7 +1331,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable badge component used to create badges with different sizes, types, and colors.\n\nThe component supports a `` for injecting content within the badge.", + "description": "A customizable badge component used to create badges with different sizes, types, and colors.\r\n\r\nThe component supports a `` for injecting content within the badge.", "name": "ModusWcBadge", "members": [ { @@ -1354,7 +1354,7 @@ "default": "'primary'", "description": "The color variant of the badge.", "type": { - "text": "| 'primary'\n | 'secondary'\n | 'tertiary'\n | 'high-contrast'\n | 'success'\n | 'warning'\n | 'danger'" + "text": "| 'primary'\r\n | 'secondary'\r\n | 'tertiary'\r\n | 'high-contrast'\r\n | 'success'\r\n | 'warning'\r\n | 'danger'" } }, { @@ -1498,7 +1498,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable buttongroup component that groups multiple Modus buttons together.\n\nThe component supports a `` for injecting content within the buttongroup.", + "description": "A customizable buttongroup component that groups multiple Modus buttons together.\r\n\r\nThe component supports a `` for injecting content within the buttongroup.", "name": "ModusWcButtonGroup", "members": [ { @@ -1599,14 +1599,14 @@ { "name": "buttonGroupClick", "type": { - "text": "EventEmitter<{\n button: HTMLElement;\n isSelected: boolean;\n }>" + "text": "EventEmitter<{\r\n button: HTMLElement;\r\n isSelected: boolean;\r\n }>" }, "description": "Event emitted when any button in the group is clicked" }, { "name": "buttonSelectionChange", "type": { - "text": "EventEmitter<{\n selectedButtons: HTMLElement[];\n }>" + "text": "EventEmitter<{\r\n selectedButtons: HTMLElement[];\r\n }>" }, "description": "Event emitted when button selection changes" } @@ -1639,7 +1639,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable button component used to create buttons with different sizes, variants, and types.\n\nThe component supports a `` for injecting content within the button, similar to a native HTML button", + "description": "A customizable button component used to create buttons with different sizes, variants, and types.\r\n\r\nThe component supports a `` for injecting content within the button, similar to a native HTML button", "name": "ModusWcButton", "members": [ { @@ -2173,7 +2173,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable collapse component used for showing and hiding content.\n\nThe component supports a 'header' and 'content' `` for injecting custom HTML.\nDo not set", + "description": "A customizable collapse component used for showing and hiding content.\r\n\r\nThe component supports a 'header' and 'content' `` for injecting custom HTML.\r\nDo not set", "name": "ModusWcCollapse", "members": [ { @@ -2240,7 +2240,7 @@ { "name": "options", "fieldName": "options", - "description": "Configuration options for rendering the pre-laid out collapse component.\nDo not set this prop if you intend to use the 'header' slot.", + "description": "Configuration options for rendering the pre-laid out collapse component.\r\nDo not set this prop if you intend to use the 'header' slot.", "type": { "text": "ICollapseOptions" } @@ -2279,13 +2279,97 @@ } ] }, + { + "kind": "javascript-module", + "path": "src/components/modus-wc-content-tree/modus-wc-content-tree.tsx", + "declarations": [ + { + "kind": "class", + "description": "A customizable content tree component used to display hierarchical data in a tree structure.\r\nUses menu items to create the tree structure with support for expanding/collapsing nodes and selection.", + "name": "ModusWcContentTree", + "members": [ + { + "kind": "field", + "name": "el", + "type": { + "text": "HTMLElement" + }, + "description": "Reference to the host element" + }, + { + "kind": "method", + "name": "render" + } + ], + "attributes": [ + { + "name": "custom-class", + "fieldName": "customClass", + "default": "''", + "description": "Custom CSS class to apply to the component.", + "type": { + "text": "string" + } + }, + { + "name": "search-placeholder", + "fieldName": "searchPlaceholder", + "default": "'Search...'", + "description": "Placeholder text for the search input.", + "type": { + "text": "string" + } + }, + { + "name": "show-actions", + "fieldName": "showActions", + "default": "false", + "description": "Whether to show the action bar with add, delete, and collapse all buttons.", + "type": { + "text": "boolean" + } + }, + { + "name": "show-search", + "fieldName": "showSearch", + "default": "false", + "description": "Whether to show the search input.", + "type": { + "text": "boolean" + } + } + ], + "tagName": "modus-wc-content-tree", + "events": [], + "customElement": true + } + ], + "exports": [ + { + "kind": "js", + "name": "ModusWcContentTree", + "declaration": { + "name": "ModusWcContentTree", + "module": "src/components/modus-wc-content-tree/modus-wc-content-tree.tsx" + } + }, + { + "kind": "custom-element-definition", + "name": "modus-wc-content-tree", + "declaration": { + "name": "ModusWcContentTree", + "module": "src/components/modus-wc-content-tree/modus-wc-content-tree.tsx" + } + } + ] + }, { "kind": "javascript-module", "path": "src/components/modus-wc-date/modus-wc-date.tsx", "declarations": [ { "kind": "class", - "description": "A customizable date picker component used to create date inputs.\n\nAdheres to WCAG 2.2 standards.", + "description": "A customizable date picker component used to create date inputs.\r\n\r\nAdheres to WCAG 2.2 standards.", "name": "ModusWcDate", "members": [ { @@ -2422,7 +2506,7 @@ "default": "'dd-mm-yyyy'", "description": "The date format for display and input.", "type": { - "text": "| 'yyyy-mm-dd'\n | 'dd-mm-yyyy'\n | 'yyyy/mm/dd'\n | 'dd/mm/yyyy'\n | 'MMM DD, YYYY'" + "text": "| 'yyyy-mm-dd'\r\n | 'dd-mm-yyyy'\r\n | 'yyyy/mm/dd'\r\n | 'dd/mm/yyyy'\r\n | 'MMM DD, YYYY'" } }, { @@ -2622,7 +2706,7 @@ "default": "'tertiary'", "description": "The color of the divider line.", "type": { - "text": "| 'primary'\n | 'secondary'\n | 'tertiary'\n | 'high-contrast'\n | 'success'\n | 'warning'\n | 'danger'" + "text": "| 'primary'\r\n | 'secondary'\r\n | 'tertiary'\r\n | 'high-contrast'\r\n | 'success'\r\n | 'warning'\r\n | 'danger'" } }, { @@ -2731,7 +2815,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable dropdown menu component used to render a button and toggleable menu.\n\nThe component supports a 'button' and 'menu' `` for injecting custom HTML content.", + "description": "A customizable dropdown menu component used to render a button and toggleable menu.\r\n\r\nThe component supports a 'button' and 'menu' `` for injecting custom HTML content.", "name": "ModusWcDropdownMenu", "members": [ { @@ -2798,7 +2882,7 @@ "default": "'primary'", "description": "The color variant of the button.", "type": { - "text": "| 'primary'\n | 'secondary'\n | 'tertiary'\n | 'warning'\n | 'danger'" + "text": "| 'primary'\r\n | 'secondary'\r\n | 'tertiary'\r\n | 'warning'\r\n | 'danger'" } }, { @@ -3094,7 +3178,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable icon component used to render Modus icons.\n\nThis component requires Modus icons to be installed in the host application. See [Modus Icon Usage](/docs/documentation-modus-icon-usage--docs) for steps.", + "description": "A customizable icon component used to render Modus icons.\r\n\r\nThis component requires Modus icons to be installed in the host application. See [Modus Icon Usage](/docs/documentation-modus-icon-usage--docs) for steps.", "name": "ModusWcIcon", "members": [ { @@ -3185,7 +3269,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable feedback component used to provide additional context related to form input interactions.\n\nTo use a custom icon, this component requires Modus icons to be installed in the host application. See [Modus Icon Usage](/docs/documentation-modus-icon-usage--docs) for steps.", + "description": "A customizable feedback component used to provide additional context related to form input interactions.\r\n\r\nTo use a custom icon, this component requires Modus icons to be installed in the host application. See [Modus Icon Usage](/docs/documentation-modus-icon-usage--docs) for steps.", "name": "ModusWcInputFeedback", "members": [ { @@ -3277,7 +3361,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable input label component.\n\nThe component supports a `` for injecting additional custom content inside the label, such as icons or formatted text", + "description": "A customizable input label component.\r\n\r\nThe component supports a `` for injecting additional custom content inside the label, such as icons or formatted text", "name": "ModusWcInputLabel", "members": [ { @@ -3460,7 +3544,7 @@ "declarations": [ { "kind": "class", - "description": "A component for displaying Trimble product logos with support for both fixed and scalable sizing.\nProvides consistent branding across applications with various product logo options.", + "description": "A component for displaying Trimble product logos with support for both fixed and scalable sizing.\r\nProvides consistent branding across applications with various product logo options.", "name": "ModusWcLogo", "members": [ { @@ -3701,7 +3785,7 @@ "kind": "field", "name": "itemSelect", "type": { - "text": "EventEmitter<{\n value: string;\n selected?: boolean;\n }>" + "text": "EventEmitter<{\r\n value: string;\r\n selected?: boolean;\r\n }>" }, "description": "Event emitted when a menu item is selected." } @@ -3734,7 +3818,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable menu component used to display a list of li elements vertically or horizontally.\n\nThe component supports a `` for injecting custom li elements inside the ul", + "description": "A customizable menu component used to display a list of li elements vertically or horizontally.\r\n\r\nThe component supports a `` for injecting custom li elements inside the ul", "name": "ModusWcMenu", "members": [ { @@ -3834,7 +3918,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable modal component used to display content in a dialog.\n\nThe component supports a 'header', 'content', and 'footer' for injecting custom HTML", + "description": "A customizable modal component used to display content in a dialog.\r\n\r\nThe component supports a 'header', 'content', and 'footer' for injecting custom HTML", "name": "ModusWcModal", "members": [ { @@ -3855,7 +3939,7 @@ "name": "backdrop", "fieldName": "backdrop", "default": "'default'", - "description": "The modal's backdrop.\nSpecify 'static' for a backdrop that doesn't close the modal when clicked outside the modal content.", + "description": "The modal's backdrop.\r\nSpecify 'static' for a backdrop that doesn't close the modal when clicked outside the modal content.", "type": { "text": "'default' | 'static'" } @@ -3944,7 +4028,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable navbar component used for top level navigation of all Trimble applications.\n\nThe component supports a 'main-menu', 'notifications', and 'apps' `` for injecting custom HTML menus.\nIt also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML", + "description": "A customizable navbar component used for top level navigation of all Trimble applications.\r\n\r\nThe component supports a 'main-menu', 'notifications', and 'apps' `` for injecting custom HTML menus.\r\nIt also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML", "name": "ModusWcNavbar", "members": [ { @@ -4670,7 +4754,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable progress component used to show the progress of a task or show the passing of time.\n\nThe radial variant supports slotting in custom HTML to be displayed within the progress circle", + "description": "A customizable progress component used to show the progress of a task or show the passing of time.\r\n\r\nThe radial variant supports slotting in custom HTML to be displayed within the progress circle", "name": "ModusWcProgress", "members": [ { @@ -6111,7 +6195,7 @@ "kind": "field", "name": "cellEditCommit", "type": { - "text": "EventEmitter<{\n rowIndex: number;\n colId: string;\n newValue: unknown;\n updatedRow: Record;\n }>" + "text": "EventEmitter<{\r\n rowIndex: number;\r\n colId: string;\r\n newValue: unknown;\r\n updatedRow: Record;\r\n }>" }, "description": "Emits when cell editing is committed with the new value." }, @@ -6119,7 +6203,7 @@ "kind": "field", "name": "cellEditStart", "type": { - "text": "EventEmitter<{\n rowIndex: number;\n colId: string;\n }>" + "text": "EventEmitter<{\r\n rowIndex: number;\r\n colId: string;\r\n }>" }, "description": "Emits when cell editing starts." }, @@ -6135,7 +6219,7 @@ "kind": "field", "name": "rowClick", "type": { - "text": "EventEmitter<{\n row: Record;\n index: number;\n }>" + "text": "EventEmitter<{\r\n row: Record;\r\n index: number;\r\n }>" }, "description": "Emits when a row is clicked." }, @@ -6143,7 +6227,7 @@ "kind": "field", "name": "rowSelectionChange", "type": { - "text": "EventEmitter<{\n selectedRows: Record[];\n selectedRowIds: string[];\n }>" + "text": "EventEmitter<{\r\n selectedRows: Record[];\r\n selectedRowIds: string[];\r\n }>" }, "description": "Emits when row selection changes with the selected rows and their IDs." }, @@ -6250,7 +6334,7 @@ "kind": "field", "name": "tabChange", "type": { - "text": "EventEmitter<{\n previousTab: number;\n newTab: number;\n }>" + "text": "EventEmitter<{\r\n previousTab: number;\r\n newTab: number;\r\n }>" }, "description": "When a tab is switched to, this event outputs the relevant indices" } @@ -6305,7 +6389,7 @@ "fieldName": "autoCapitalize", "description": "Controls automatic capitalization in inputted text.", "type": { - "text": "| 'off'\n | 'none'\n | 'on'\n | 'sentences'\n | 'words'\n | 'characters'" + "text": "| 'off'\r\n | 'none'\r\n | 'on'\r\n | 'sentences'\r\n | 'words'\r\n | 'characters'" } }, { @@ -6365,7 +6449,7 @@ "fieldName": "enterkeyhint", "description": "A hint to the browser for which enter key to display.", "type": { - "text": "| 'enter'\n | 'done'\n | 'go'\n | 'next'\n | 'previous'\n | 'search'\n | 'send'" + "text": "| 'enter'\r\n | 'done'\r\n | 'go'\r\n | 'next'\r\n | 'previous'\r\n | 'search'\r\n | 'send'" } }, { @@ -6625,7 +6709,7 @@ "fieldName": "enterkeyhint", "description": "A hint to the browser for which enter key to display.", "type": { - "text": "| 'enter'\n | 'done'\n | 'go'\n | 'next'\n | 'previous'\n | 'search'\n | 'send'" + "text": "| 'enter'\r\n | 'done'\r\n | 'go'\r\n | 'next'\r\n | 'previous'\r\n | 'search'\r\n | 'send'" } }, { @@ -6793,7 +6877,7 @@ "declarations": [ { "kind": "class", - "description": "A theme switcher component used to toggle the application theme and/or mode.\n\nAllows consumers to set the initial theme (Modus Classic, Modus Modern, etc.) and end-users to toggle modes (Light, Dark).", + "description": "A theme switcher component used to toggle the application theme and/or mode.\r\n\r\nAllows consumers to set the initial theme (Modus Classic, Modus Modern, etc.) and end-users to toggle modes (Light, Dark).", "name": "ModusWcThemeSwitcher", "members": [ { @@ -6908,7 +6992,7 @@ { "name": "datalist-id", "fieldName": "datalistId", - "description": "ID of a `` element that contains pre-defined time options.\nThe value must be the ID of a `` element in the same document.", + "description": "ID of a `` element that contains pre-defined time options.\r\nThe value must be the ID of a `` element in the same document.", "type": { "text": "string" } @@ -7009,7 +7093,7 @@ "name": "show-seconds", "fieldName": "showSeconds", "default": "false", - "description": "Displays the time input format as `HH:mm:ss` if `true`.\nInternally sets the `step` to 1 second.\nIf a `step` value is provided, it will override this attribute.", + "description": "Displays the time input format as `HH:mm:ss` if `true`.\r\nInternally sets the `step` to 1 second.\r\nIf a `step` value is provided, it will override this attribute.", "type": { "text": "boolean" } @@ -7026,7 +7110,7 @@ { "name": "step", "fieldName": "step", - "description": "Specifies the granularity that the `value` must adhere to.\nValue of step given in seconds. Default value is 60 seconds.\nOverrides the `seconds` attribute if both are provided.", + "description": "Specifies the granularity that the `value` must adhere to.\r\nValue of step given in seconds. Default value is 60 seconds.\r\nOverrides the `seconds` attribute if both are provided.", "type": { "text": "number" } @@ -7035,7 +7119,7 @@ "name": "value", "fieldName": "value", "default": "''", - "description": "The value of the time input.\nAlways in 24-hour format that includes leading zeros:\n`HH:mm` or `HH:mm:ss`, regardless of input format which is likely\nto be selected based on user's locale (or by the user agent).\nIf time includes seconds the format is always `HH:mm:ss`.", + "description": "The value of the time input.\r\nAlways in 24-hour format that includes leading zeros:\r\n`HH:mm` or `HH:mm:ss`, regardless of input format which is likely\r\nto be selected based on user's locale (or by the user agent).\r\nIf time includes seconds the format is always `HH:mm:ss`.", "type": { "text": "string" } @@ -7096,7 +7180,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable toast component used to stack elements, positioned on the corner of a page.\n\nThe component supports a `` for injecting additional custom content inside the toast.", + "description": "A customizable toast component used to stack elements, positioned on the corner of a page.\r\n\r\nThe component supports a `` for injecting additional custom content inside the toast.", "name": "ModusWcToast", "members": [ { @@ -7248,7 +7332,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable tooltip component used to create tooltips with different content.\n\nThe tooltip can be dismissed by pressing the Escape key when hovering over it.\nWhen forceOpen is enabled, the tooltip will remain open and can only be closed by setting forceOpen to false.", + "description": "A customizable tooltip component used to create tooltips with different content.\r\n\r\nThe tooltip can be dismissed by pressing the Escape key when hovering over it.\r\nWhen forceOpen is enabled, the tooltip will remain open and can only be closed by setting forceOpen to false.", "name": "ModusWcTooltip", "members": [ { @@ -7409,7 +7493,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable typography component used to render text with different sizes, hierarchy, and weights.\n\nNote: When using heading elements (h1-h6), the default heading CSS styling can be accessed without modifying\nthe default size (size=\"md\") and weight (weight=\"normal\") properties. Default styling can be overridden by\nproviding your own custom values for the size or weight properties from the available options.", + "description": "A customizable typography component used to render text with different sizes, hierarchy, and weights.\r\n\r\nNote: When using heading elements (h1-h6), the default heading CSS styling can be accessed without modifying\r\nthe default size (size=\"md\") and weight (weight=\"normal\") properties. Default styling can be overridden by\r\nproviding your own custom values for the size or weight properties from the available options.", "name": "ModusWCTypography", "members": [ { @@ -7447,7 +7531,7 @@ { "name": "label", "fieldName": "label", - "description": "The text label to display if no slot content is provided.", + "description": "The text label to display.", "type": { "text": "string" } From eefb20b2a794f643f652476d5d26a1fbc105ec8f Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:46:00 +0530 Subject: [PATCH 03/39] 662 - content tree add node using slots --- .../modus-wc-content-tree.scss | 14 +- .../modus-wc-content-tree.stories.ts | 114 +++++++++++-- .../modus-wc-content-tree.tsx | 156 +++++++++++++----- 3 files changed, 228 insertions(+), 56 deletions(-) diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index 02c5ff3922..4620a61e18 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -4,11 +4,11 @@ */ modus-wc-content-tree { - background: white; + background-color: var(--modus-wc-color-base-page); border: 1px solid var(--modus-wc-color-border-default, #d1d5db); display: block; - padding: var(--modus-wc-spacing-md, 1rem); width: 100%; + min-width: 320px; .modus-wc-content-tree-actions { align-items: center; @@ -19,13 +19,21 @@ modus-wc-content-tree { padding-bottom: var(--modus-wc-spacing-sm, 0.75rem); } + .modus-wc-content-tree-add-node { + margin-bottom: var(--modus-wc-spacing-md, 1rem); + } + .modus-wc-content-tree-content { align-items: center; - display: flex; + display: block; height: 600px; justify-content: center; } + .modus-wc-content-tree-header { + padding: var(--modus-wc-spacing-md, 1rem); + } + .modus-wc-content-tree-empty { align-items: center; display: flex; diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts index 7e88193a0c..83b4c168e3 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts @@ -53,58 +53,144 @@ export const Default: Story = { }, }; -export const Collapsed: Story = { +export const UsingSlot: Story = { render: (args) => { - const multiSelect = args['multi-select']; return html` + + + + + + + + - + + + + + + + + + + + + + - + + + + + diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx index b08f316b9c..169ebb2fa5 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx @@ -12,6 +12,7 @@ import { Attributes, inheritAriaAttributes } from '../utils'; }) export class ModusWcContentTree { private inheritedAttributes: Attributes = {}; + private slotEl?: HTMLSlotElement; /** Reference to the host element */ @Element() el!: HTMLElement; @@ -19,34 +20,96 @@ export class ModusWcContentTree { /** Custom CSS class to apply to the component. */ @Prop() customClass?: string = ''; - /** Whether to show the search input. */ - @Prop() showSearch?: boolean = false; + /** Placeholder text for the search input. */ + @Prop() searchPlaceholder?: string = 'Search...'; /** Whether to show the action bar with add, delete, and collapse all buttons. */ @Prop() showActions?: boolean = false; - /** Placeholder text for the search input. */ - @Prop() searchPlaceholder?: string = 'Search...'; + /** Whether to show the search input. */ + @Prop() showSearch?: boolean = false; + @State() private hasSlotContent: boolean = false; @State() private searchValue: string = ''; - @State() private hasSlotContent: boolean = true; + @State() private showAddNodeInput: boolean = false; componentWillLoad() { this.inheritedAttributes = inheritAriaAttributes(this.el); + // Check initial slot content + const slotContent = Array.from(this.el.childNodes).filter( + (node) => + node.nodeType === Node.ELEMENT_NODE && + (node as HTMLElement).tagName !== 'STYLE' + ); + this.hasSlotContent = slotContent.length > 0; } componentDidLoad() { - this.checkSlotContent(); + this.slotEl = this.el.querySelector('slot') as HTMLSlotElement; + this.updateSlotContent(); + this.slotEl?.addEventListener('slotchange', this.updateSlotContent); + document.addEventListener('click', this.handleClickOutside); } - private checkSlotContent() { - // Since shadow: false, check direct children (excluding the wrapper div) - const children = Array.from(this.el.children).filter( - (child) => !child.classList.contains('modus-wc-content-tree-wrapper') - ); - this.hasSlotContent = children.length > 0; + disconnectedCallback() { + this.slotEl?.removeEventListener('slotchange', this.updateSlotContent); + document.removeEventListener('click', this.handleClickOutside); } + private addMenuItem = () => { + this.showAddNodeInput = true; + }; + + private createMenuItem() { + console.log('Creating menu item:', this.searchValue); + } + + private handleAddNodeInput = (event: CustomEvent) => { + const target = event.target as HTMLInputElement; + this.searchValue = target.value; + }; + + private handleAddNodeKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Enter' && this.searchValue.trim()) { + this.createMenuItem(); + } else if (event.key === 'Escape') { + this.showAddNodeInput = false; + this.searchValue = ''; + } + }; + + private handleClickOutside = (event: MouseEvent) => { + const target = event.target as HTMLElement; + const addNodeSection = this.el.querySelector( + '.modus-wc-content-tree-add-node' + ); + const emptySection = this.el.querySelector('.modus-wc-content-tree-empty'); + + if ( + addNodeSection && + !addNodeSection.contains(target) && + emptySection && + !emptySection.contains(target) + ) { + this.showAddNodeInput = false; + this.searchValue = ''; + } + }; + + private updateSlotContent = () => { + if (!this.slotEl) return; + + const assigned = this.slotEl + .assignedNodes({ flatten: true }) + .filter( + (node) => + node.nodeType === Node.ELEMENT_NODE && + (node as HTMLElement).tagName !== 'STYLE' + ); + + this.hasSlotContent = assigned.length > 0; + }; + render() { return ( @@ -54,34 +117,48 @@ export class ModusWcContentTree { class={`modus-wc-content-tree-wrapper ${this.customClass}`} {...this.inheritedAttributes} > - {this.showSearch && ( - - )} - - {this.showActions && this.hasSlotContent && ( -
- - - -
- )} +
+ {this.showSearch && ( + + )} + {this.showActions && ( +
+ + + +
+ )} + + {this.showAddNodeInput && ( +
+ +
+ )} +
{!this.hasSlotContent && ( @@ -99,6 +176,7 @@ export class ModusWcContentTree { size="md" type="button" variant="filled" + onClick={this.addMenuItem} > Create Node From ca9d12bf4e5cd7979663bd81bf9198d2bedba8fc Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Mon, 19 Jan 2026 16:58:26 +0530 Subject: [PATCH 04/39] 662 - add search functionality --- src/components.d.ts | 8 - .../modus-wc-content-tree.scss | 40 ++--- .../modus-wc-content-tree.stories.ts | 91 +++++----- .../modus-wc-content-tree.tsx | 157 +++++++++++++----- .../modus-wc-content-tree/readme.md | 1 - src/custom-elements.json | 9 - 6 files changed, 165 insertions(+), 141 deletions(-) diff --git a/src/components.d.ts b/src/components.d.ts index e347ebf656..1559ddea2e 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -523,10 +523,6 @@ export namespace Components { * Whether to show the action bar with add, delete, and collapse all buttons. */ "showActions"?: boolean; - /** - * Whether to show the search input. - */ - "showSearch"?: boolean; } /** * A customizable date picker component used to create date inputs. @@ -3602,10 +3598,6 @@ declare namespace LocalJSX { * Whether to show the action bar with add, delete, and collapse all buttons. */ "showActions"?: boolean; - /** - * Whether to show the search input. - */ - "showSearch"?: boolean; } /** * A customizable date picker component used to create date inputs. diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index 4620a61e18..1661444a90 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -7,8 +7,8 @@ modus-wc-content-tree { background-color: var(--modus-wc-color-base-page); border: 1px solid var(--modus-wc-color-border-default, #d1d5db); display: block; - width: 100%; min-width: 320px; + width: 100%; .modus-wc-content-tree-actions { align-items: center; @@ -26,8 +26,7 @@ modus-wc-content-tree { .modus-wc-content-tree-content { align-items: center; display: block; - height: 600px; - justify-content: center; + min-height: 500px; } .modus-wc-content-tree-header { @@ -39,29 +38,18 @@ modus-wc-content-tree { display: flex; flex-direction: column; gap: var(--modus-wc-spacing-md, 1rem); - height: 100%; justify-content: center; - text-align: center; - } - - .modus-wc-content-tree-empty-icon { - color: var(--modus-wc-color-gray-6, #6a6e79); - opacity: 0.5; - } - - .modus-wc-content-tree-empty-title { - color: var(--modus-wc-color-gray-8, #464b52); - font-size: var(--modus-wc-font-size-lg, 1.125rem); - font-weight: 600; - margin: 0; - } - - .modus-wc-content-tree-search { - margin-bottom: var(--modus-wc-spacing-md, 1rem); - width: 304px; - } - - .modus-wc-menu-dropdown { - margin-inline-start: var(--modus-wc-spacing-md, 1rem); + min-height: 500px; + padding: 1rem; + + .modus-wc-content-tree-empty-icon { + color: var(--modus-wc-color-text-secondary, #6b7280); + } + + .modus-wc-content-tree-empty-text { + color: var(--modus-wc-color-text-secondary, #6b7280); + font-size: var(--modus-wc-font-size-md, 1rem); + text-align: center; + } } } diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts index 83b4c168e3..aa7fcf0fde 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts @@ -1,11 +1,10 @@ +import { withActions } from '@storybook/addon-actions/decorator'; import { Meta, StoryObj } from '@storybook/web-components'; import { html } from 'lit'; import { ifDefined } from 'lit/directives/if-defined.js'; interface ContentTreeArgs { 'custom-class'?: string; - 'multi-select'?: boolean; - 'show-search'?: boolean; 'show-actions'?: boolean; 'search-placeholder'?: string; } @@ -14,18 +13,10 @@ const meta: Meta = { title: 'Components/Content Tree', component: 'modus-wc-content-tree', args: { - 'multi-select': false, - 'show-search': false, 'show-actions': false, 'search-placeholder': 'Search...', }, argTypes: { - 'multi-select': { - control: { type: 'boolean' }, - }, - 'show-search': { - control: { type: 'boolean' }, - }, 'show-actions': { control: { type: 'boolean' }, }, @@ -33,6 +24,12 @@ const meta: Meta = { control: { type: 'text' }, }, }, + decorators: [withActions], + parameters: { + actions: { + handles: ['itemSelect'], + }, + }, }; export default meta; @@ -43,10 +40,9 @@ export const Default: Story = { render: (args) => { return html` `; @@ -57,15 +53,14 @@ export const UsingSlot: Story = { render: (args) => { return html` @@ -91,8 +86,8 @@ export const UsingSlot: Story = { > @@ -106,8 +101,8 @@ export const UsingSlot: Story = { @@ -133,8 +128,8 @@ export const UsingSlot: Story = { > @@ -164,8 +159,8 @@ export const UsingSlot: Story = { @@ -200,19 +195,13 @@ export const UsingSlot: Story = { export const SingleLevel: Story = { render: (args) => { - const multiSelect = args['multi-select']; return html` - + - + { - this.showAddNodeInput = true; + this.searchValue = ''; + this.filterNodes(''); }; private createMenuItem() { console.log('Creating menu item:', this.searchValue); + this.searchValue = ''; + this.isAddNodeMode = false; } - private handleAddNodeInput = (event: CustomEvent) => { + private handleInputChange = (event: CustomEvent) => { const target = event.target as HTMLInputElement; this.searchValue = target.value; + + if (!this.isAddNodeMode) { + this.filterNodes(this.searchValue); + } }; - private handleAddNodeKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Enter' && this.searchValue.trim()) { - this.createMenuItem(); - } else if (event.key === 'Escape') { - this.showAddNodeInput = false; + private filterNodes(searchTerm: string) { + const menuItems = this.el.querySelectorAll('modus-wc-menu-item'); + const normalizedSearch = searchTerm.toLowerCase().trim(); + + if (!normalizedSearch) { + // Show all nodes when search is empty + menuItems.forEach((item) => { + (item as HTMLElement).style.display = ''; + }); + return; + } + + menuItems.forEach((item) => { + const label = item.getAttribute('label') || ''; + const normalizedLabel = label.toLowerCase(); + const matches = normalizedLabel.includes(normalizedSearch); + + if (matches) { + // Show matching node + (item as HTMLElement).style.display = ''; + + // Expand and show all parent nodes + let parent = item.parentElement; + while (parent && parent !== this.el) { + if (parent.tagName === 'MODUS-WC-MENU-ITEM') { + parent.style.display = ''; + parent.setAttribute('expanded', 'true'); + } + parent = parent.parentElement; + } + } else { + // Check if any children match + const hasMatchingChildren = this.hasMatchingDescendants( + item as HTMLElement, + normalizedSearch + ); + + if (hasMatchingChildren) { + (item as HTMLElement).style.display = ''; + item.setAttribute('expanded', 'true'); + } else { + (item as HTMLElement).style.display = 'none'; + } + } + }); + } + + private hasMatchingDescendants( + element: HTMLElement, + searchTerm: string + ): boolean { + const childMenuItems = element.querySelectorAll('modus-wc-menu-item'); + + for (const child of Array.from(childMenuItems)) { + const label = child.getAttribute('label') || ''; + if (label.toLowerCase().includes(searchTerm)) { + return true; + } + } + + return false; + } + + private handleInputKeyDown = (event: KeyboardEvent) => { + const value = this.searchValue.trim(); + + if (event.key === 'Enter') { + if (this.isAddNodeMode && value) { + this.createMenuItem(); + } + return; + } + + if (event.key === 'Escape') { + this.isAddNodeMode = false; this.searchValue = ''; + this.filterNodes(''); } }; private handleClickOutside = (event: MouseEvent) => { const target = event.target as HTMLElement; - const addNodeSection = this.el.querySelector( - '.modus-wc-content-tree-add-node' + const searchSection = this.el.querySelector( + '.modus-wc-content-tree-search' ); const emptySection = this.el.querySelector('.modus-wc-content-tree-empty'); if ( - addNodeSection && - !addNodeSection.contains(target) && - emptySection && - !emptySection.contains(target) + this.isAddNodeMode && + searchSection && + !searchSection.contains(target) && + (!emptySection || !emptySection.contains(target)) ) { - this.showAddNodeInput = false; + this.isAddNodeMode = false; this.searchValue = ''; } }; @@ -118,17 +193,19 @@ export class ModusWcContentTree { {...this.inheritedAttributes} >
- {this.showSearch && ( - - )} + {this.showActions && (
@@ -136,39 +213,33 @@ export class ModusWcContentTree { decorative={true} name="delete" size="sm" + variant="solid" >
)} - - {this.showAddNodeInput && ( -
- -
- )}
{!this.hasSlotContent && (
- + Date: Tue, 20 Jan 2026 13:31:26 +0530 Subject: [PATCH 05/39] 662 - add menu item toggle feature --- src/components.d.ts | 12 ++-- .../modus-wc-content-tree.scss | 4 ++ .../modus-wc-content-tree.stories.ts | 7 --- .../modus-wc-content-tree.tsx | 58 ++++++++++++------- .../modus-wc-content-tree/readme.md | 9 ++- .../modus-wc-menu-item/modus-wc-menu-item.tsx | 20 +++++++ src/components/modus-wc-menu-item/readme.md | 10 ++++ src/custom-elements.json | 10 ++++ 8 files changed, 88 insertions(+), 42 deletions(-) diff --git a/src/components.d.ts b/src/components.d.ts index 1559ddea2e..ec33707e87 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -519,10 +519,6 @@ export namespace Components { * Placeholder text for the search input. */ "searchPlaceholder"?: string; - /** - * Whether to show the action bar with add, delete, and collapse all buttons. - */ - "showActions"?: boolean; } /** * A customizable date picker component used to create date inputs. @@ -920,6 +916,10 @@ export namespace Components { * The disabled state of the menu item. */ "disabled"?: boolean; + /** + * Public method to expand the submenu if it's collapsed + */ + "expandSubmenu": () => Promise; /** * The focused state of the menu item. */ @@ -3594,10 +3594,6 @@ declare namespace LocalJSX { * Placeholder text for the search input. */ "searchPlaceholder"?: string; - /** - * Whether to show the action bar with add, delete, and collapse all buttons. - */ - "showActions"?: boolean; } /** * A customizable date picker component used to create date inputs. diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index 1661444a90..20cae86926 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -17,6 +17,10 @@ modus-wc-content-tree { justify-content: flex-end; margin-bottom: var(--modus-wc-spacing-md, 1rem); padding-bottom: var(--modus-wc-spacing-sm, 0.75rem); + + .modus-wc-icon{ + cursor: pointer; + } } .modus-wc-content-tree-add-node { diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts index aa7fcf0fde..fd57a7de18 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts @@ -5,7 +5,6 @@ import { ifDefined } from 'lit/directives/if-defined.js'; interface ContentTreeArgs { 'custom-class'?: string; - 'show-actions'?: boolean; 'search-placeholder'?: string; } @@ -13,13 +12,9 @@ const meta: Meta = { title: 'Components/Content Tree', component: 'modus-wc-content-tree', args: { - 'show-actions': false, 'search-placeholder': 'Search...', }, argTypes: { - 'show-actions': { - control: { type: 'boolean' }, - }, 'search-placeholder': { control: { type: 'text' }, }, @@ -41,7 +36,6 @@ export const Default: Story = { return html` @@ -54,7 +48,6 @@ export const UsingSlot: Story = { return html` diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx index 7d56919d5d..bee231bdab 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx @@ -23,9 +23,6 @@ export class ModusWcContentTree { /** Placeholder text for the search input. */ @Prop() searchPlaceholder?: string = 'Search...'; - /** Whether to show the action bar with add, delete, and collapse all buttons. */ - @Prop() showActions?: boolean = false; - @State() private hasSlotContent: boolean = false; @State() private searchValue: string = ''; @State() private isAddNodeMode: boolean = false; @@ -58,6 +55,24 @@ export class ModusWcContentTree { this.filterNodes(''); }; + private expandAllMenuItems = () => { + const menuItems = this.el.querySelectorAll('modus-wc-menu-item'); + + // Check if any items are expanded by checking the li element for the class + const hasExpandedItems = Array.from(menuItems).some((item) => { + const liElement = item.querySelector('li'); + return liElement?.classList.contains('modus-wc-menu-item-expanded'); + }); + + menuItems.forEach((item) => { + if (hasExpandedItems) { + (item as any).collapseSubmenu(); + } else { + (item as any).expandSubmenu(); + } + }); + }; + private createMenuItem() { console.log('Creating menu item:', this.searchValue); this.searchValue = ''; @@ -99,7 +114,7 @@ export class ModusWcContentTree { while (parent && parent !== this.el) { if (parent.tagName === 'MODUS-WC-MENU-ITEM') { parent.style.display = ''; - parent.setAttribute('expanded', 'true'); + (parent as any).expandSubmenu(); } parent = parent.parentElement; } @@ -112,7 +127,7 @@ export class ModusWcContentTree { if (hasMatchingChildren) { (item as HTMLElement).style.display = ''; - item.setAttribute('expanded', 'true'); + (item as any).expandSubmenu(); } else { (item as HTMLElement).style.display = 'none'; } @@ -207,23 +222,22 @@ export class ModusWcContentTree { >
- {this.showActions && ( -
- - - -
- )} +
+ + + +
diff --git a/src/components/modus-wc-content-tree/readme.md b/src/components/modus-wc-content-tree/readme.md index 2cbcaeda1a..cfdd15e3c7 100644 --- a/src/components/modus-wc-content-tree/readme.md +++ b/src/components/modus-wc-content-tree/readme.md @@ -12,11 +12,10 @@ Uses menu items to create the tree structure with support for expanding/collapsi ## Properties -| Property | Attribute | Description | Type | Default | -| ------------------- | -------------------- | -------------------------------------------------------------------------- | ---------------------- | ------------- | -| `customClass` | `custom-class` | Custom CSS class to apply to the component. | `string \| undefined` | `''` | -| `searchPlaceholder` | `search-placeholder` | Placeholder text for the search input. | `string \| undefined` | `'Search...'` | -| `showActions` | `show-actions` | Whether to show the action bar with add, delete, and collapse all buttons. | `boolean \| undefined` | `false` | +| Property | Attribute | Description | Type | Default | +| ------------------- | -------------------- | ------------------------------------------- | --------------------- | ------------- | +| `customClass` | `custom-class` | Custom CSS class to apply to the component. | `string \| undefined` | `''` | +| `searchPlaceholder` | `search-placeholder` | Placeholder text for the search input. | `string \| undefined` | `'Search...'` | ## Dependencies diff --git a/src/components/modus-wc-menu-item/modus-wc-menu-item.tsx b/src/components/modus-wc-menu-item/modus-wc-menu-item.tsx index 3e90afd6e4..829656702f 100644 --- a/src/components/modus-wc-menu-item/modus-wc-menu-item.tsx +++ b/src/components/modus-wc-menu-item/modus-wc-menu-item.tsx @@ -89,6 +89,26 @@ export class ModusWcMenuItem { } }; + /** + * Public method to expand the submenu if it's collapsed + */ + + @Method() + async expandSubmenu(): Promise { + if (this.hasSubmenu && !this.isExpanded) { + const submenu = this.el.querySelector( + '.modus-wc-menu-dropdown' + ) as HTMLElement; + const liElement = this.el.querySelector('li'); + if (submenu && liElement) { + submenu.classList.add('modus-wc-menu-dropdown-show'); + liElement.classList.add('modus-wc-menu-item-expanded'); + this.isExpanded = true; + } + } + return Promise.resolve(); + } + /** * Public method to collapse the submenu if it's expanded */ diff --git a/src/components/modus-wc-menu-item/readme.md b/src/components/modus-wc-menu-item/readme.md index 483e66fce5..77314cdbc9 100644 --- a/src/components/modus-wc-menu-item/readme.md +++ b/src/components/modus-wc-menu-item/readme.md @@ -48,6 +48,16 @@ Type: `Promise` +### `expandSubmenu() => Promise` + +Public method to expand the submenu if it's collapsed + +#### Returns + +Type: `Promise` + + + ## Dependencies diff --git a/src/custom-elements.json b/src/custom-elements.json index ff7f4a3051..c6434a3aa8 100644 --- a/src/custom-elements.json +++ b/src/custom-elements.json @@ -3638,6 +3638,16 @@ }, "description": "Reference to the host element" }, + { + "kind": "method", + "name": "expandSubmenu", + "return": { + "type": { + "text": "Promise" + } + }, + "description": "Public method to expand the submenu if it's collapsed" + }, { "kind": "field", "name": "isExpanded", From 1ba646e9f2dac3906a5cdfbbddef629790bed5bb Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:32:14 +0530 Subject: [PATCH 06/39] 662 - remove --- src/components.d.ts | 4 ---- .../modus-wc-menu-item/modus-wc-menu-item.tsx | 20 ------------------- src/components/modus-wc-menu-item/readme.md | 10 ---------- 3 files changed, 34 deletions(-) diff --git a/src/components.d.ts b/src/components.d.ts index ec33707e87..8503d1674d 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -916,10 +916,6 @@ export namespace Components { * The disabled state of the menu item. */ "disabled"?: boolean; - /** - * Public method to expand the submenu if it's collapsed - */ - "expandSubmenu": () => Promise; /** * The focused state of the menu item. */ diff --git a/src/components/modus-wc-menu-item/modus-wc-menu-item.tsx b/src/components/modus-wc-menu-item/modus-wc-menu-item.tsx index 829656702f..3e90afd6e4 100644 --- a/src/components/modus-wc-menu-item/modus-wc-menu-item.tsx +++ b/src/components/modus-wc-menu-item/modus-wc-menu-item.tsx @@ -89,26 +89,6 @@ export class ModusWcMenuItem { } }; - /** - * Public method to expand the submenu if it's collapsed - */ - - @Method() - async expandSubmenu(): Promise { - if (this.hasSubmenu && !this.isExpanded) { - const submenu = this.el.querySelector( - '.modus-wc-menu-dropdown' - ) as HTMLElement; - const liElement = this.el.querySelector('li'); - if (submenu && liElement) { - submenu.classList.add('modus-wc-menu-dropdown-show'); - liElement.classList.add('modus-wc-menu-item-expanded'); - this.isExpanded = true; - } - } - return Promise.resolve(); - } - /** * Public method to collapse the submenu if it's expanded */ diff --git a/src/components/modus-wc-menu-item/readme.md b/src/components/modus-wc-menu-item/readme.md index 77314cdbc9..483e66fce5 100644 --- a/src/components/modus-wc-menu-item/readme.md +++ b/src/components/modus-wc-menu-item/readme.md @@ -48,16 +48,6 @@ Type: `Promise` -### `expandSubmenu() => Promise` - -Public method to expand the submenu if it's collapsed - -#### Returns - -Type: `Promise` - - - ## Dependencies From 5d2c24e8ff782fb78e05f115730dcfc6cce3d553 Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:36:56 +0530 Subject: [PATCH 07/39] 662 - poc atomic approach --- .../modus-wc-content-tree.stories.ts | 299 +++++++++++++++++- .../modus-wc-content-tree.tsx | 19 -- src/custom-elements.json | 20 +- 3 files changed, 296 insertions(+), 42 deletions(-) diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts index fd57a7de18..923480f724 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts @@ -62,7 +62,7 @@ export const UsingSlot: Story = { slot="start-icon" name="folder_closed" variant="solid" - size="sm" + size="xs" > @@ -104,7 +104,7 @@ export const UsingSlot: Story = { slot="start-icon" name="folder_closed" variant="solid" - size="sm" + size="xs" > @@ -162,7 +162,7 @@ export const UsingSlot: Story = { slot="start-icon" name="folder_closed" variant="solid" - size="sm" + size="xs" > @@ -186,6 +186,281 @@ export const UsingSlot: Story = { }, }; +export const CustomMenu: Story = { + render: (args) => { + const toggleExpand = (e: Event) => { + const icon = e.target as HTMLElement; + const li = icon.closest('li'); + if (!li) return; + + const iconElement = li.querySelector( + 'modus-wc-icon[name="expand_more"], modus-wc-icon[name="chevron_right"]' + ) as any; + const siblings = Array.from(li.parentElement?.children || []); + const currentIndex = siblings.indexOf(li); + + // Find all child items (next siblings with custom-nested-row class) + const children: HTMLElement[] = []; + for (let i = currentIndex + 1; i < siblings.length; i++) { + const sibling = siblings[i] as HTMLElement; + if (sibling.querySelector('.custom-nested-row')) { + children.push(sibling); + } else { + break; + } + } + + // Toggle icon and children visibility + if (iconElement.getAttribute('name') === 'expand_more') { + iconElement.setAttribute('name', 'chevron_right'); + li.classList.remove('expanded'); + children.forEach((child) => (child.style.display = 'none')); + } else { + iconElement.setAttribute('name', 'expand_more'); + li.classList.add('expanded'); + children.forEach((child) => (child.style.display = '')); + } + }; + + return html` + + + +
  • +
    + + + +
    Parent
    +
    + + + + + + +
    +
    +
  • +
  • +
    + +
    + +
    Child
    +
    + + + + + + +
    +
    +
    +
  • +
  • +
    + +
    + +
    Child
    +
    + + + + + + +
    +
    +
    +
  • +
  • +
    + + + +
    Parent
    +
    + + + + + + +
    +
    +
  • +
    +
    + `; + }, +}; + export const SingleLevel: Story = { render: (args) => { return html` @@ -199,7 +474,7 @@ export const SingleLevel: Story = { slot="start-icon" name="description" variant="solid" - size="sm" + size="xs" >
    @@ -208,7 +483,7 @@ export const SingleLevel: Story = { slot="start-icon" name="description" variant="solid" - size="sm" + size="xs" > @@ -222,7 +497,7 @@ export const SingleLevel: Story = { slot="start-icon" name="description" variant="solid" - size="sm" + size="xs" >
    diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx index bee231bdab..5905b1a0ce 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx @@ -55,24 +55,6 @@ export class ModusWcContentTree { this.filterNodes(''); }; - private expandAllMenuItems = () => { - const menuItems = this.el.querySelectorAll('modus-wc-menu-item'); - - // Check if any items are expanded by checking the li element for the class - const hasExpandedItems = Array.from(menuItems).some((item) => { - const liElement = item.querySelector('li'); - return liElement?.classList.contains('modus-wc-menu-item-expanded'); - }); - - menuItems.forEach((item) => { - if (hasExpandedItems) { - (item as any).collapseSubmenu(); - } else { - (item as any).expandSubmenu(); - } - }); - }; - private createMenuItem() { console.log('Creating menu item:', this.searchValue); this.searchValue = ''; @@ -235,7 +217,6 @@ export class ModusWcContentTree { name="unfold_less" size="sm" variant="solid" - onClick={this.expandAllMenuItems} >
    diff --git a/src/custom-elements.json b/src/custom-elements.json index c6434a3aa8..1426379b18 100644 --- a/src/custom-elements.json +++ b/src/custom-elements.json @@ -2319,15 +2319,6 @@ "type": { "text": "string" } - }, - { - "name": "show-actions", - "fieldName": "showActions", - "default": "false", - "description": "Whether to show the action bar with add, delete, and collapse all buttons.", - "type": { - "text": "boolean" - } } ], "tagName": "modus-wc-content-tree", @@ -3645,8 +3636,7 @@ "type": { "text": "Promise" } - }, - "description": "Public method to expand the submenu if it's collapsed" + } }, { "kind": "field", @@ -3711,6 +3701,14 @@ "text": "boolean" } }, + { + "name": "indeterminate", + "fieldName": "indeterminate", + "description": "The indeterminate state of the checkbox.", + "type": { + "text": "boolean" + } + }, { "name": "label", "fieldName": "label", From 125cb24787d96815ec11b70d217983a3fa5f159c Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:53:16 +0530 Subject: [PATCH 08/39] 662 - replace menu item with content tree item --- src/components.d.ts | 436 ++++++++++++++-- .../modus-wc-content-tree.scss | 2 +- .../modus-wc-content-tree.stories.ts | 474 ++---------------- .../modus-wc-content-tree.tsx | 4 +- .../modus-wc-tree-item.scss | 46 ++ .../modus-wc-tree-item/modus-wc-tree-item.tsx | 153 ++++++ .../modus-wc-tree-item/readme.md | 51 ++ .../modus-wc-tree-view.scss | 17 + .../modus-wc-tree-view/modus-wc-tree-view.tsx | 60 +++ .../modus-wc-tree-view/readme.md | 23 + src/custom-elements.json | 277 ++++++++-- 11 files changed, 1014 insertions(+), 529 deletions(-) create mode 100644 src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss create mode 100644 src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx create mode 100644 src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md create mode 100644 src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.scss create mode 100644 src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx create mode 100644 src/components/modus-wc-content-tree/modus-wc-tree-view/readme.md diff --git a/src/components.d.ts b/src/components.d.ts index 8503d1674d..ebb1b2759f 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -42,7 +42,7 @@ export { TypographyHierarchy, TypographySize, TypographyWeight } from "./compone export namespace Components { /** * A customizable accordion component used for showing and hiding related groups of content. - * The component supports a `` for injecting `` elements. See [Collapse](/docs/components-collapse--docs) docs for additional info. + * The component supports a `` called 'content' for injecting `` elements. See [Collapse](/docs/components-collapse--docs) docs for additional info. */ interface ModusWcAccordion { /** @@ -51,7 +51,8 @@ export namespace Components { "customClass"?: string; } /** - * A customizable alert component used to inform the user about important events + * A customizable alert component used to inform the user about important events. + * The component supports `` elements for injecting custom content and buttons. */ interface ModusWcAlert { /** @@ -85,6 +86,7 @@ export namespace Components { } /** * A customizable autocomplete component used to create searchable text inputs. + * The component supports a `` for injecting custom content. */ interface ModusWcAutocomplete { /** @@ -127,6 +129,10 @@ export namespace Components { * Whether the form control is disabled. */ "disabled"?: boolean; + /** + * Feedback state for the input field. + */ + "feedback"?: IInputFeedbackProp; /** * Programmatically set focus to input */ @@ -302,7 +308,7 @@ export namespace Components { } /** * A customizable button component used to create buttons with different sizes, variants, and types. - * The component supports a `` for injecting content within the button, similar to a native HTML button + * The component supports a `` for injecting content within the button, similar to a native HTML button. */ interface ModusWcButton { /** @@ -369,7 +375,8 @@ export namespace Components { "variant": 'borderless' | 'filled' | 'outlined'; } /** - * A customizable card component used to group and display content in a way that is easily readable + * A customizable card component used to group and display content in a way that is easily readable. + * This component supports multiple `` elements including 'header' for images or custom content, 'title', 'subtitle', a default slot for main content, 'actions' for buttons or interactive elements, and 'footer'. */ interface ModusWcCard { /** @@ -440,6 +447,7 @@ export namespace Components { } /** * A customizable chip component used to display information in a compact area + * The component supports a `` for injecting custom content such as avatar and icons. */ interface ModusWcChip { /** @@ -482,7 +490,6 @@ export namespace Components { /** * A customizable collapse component used for showing and hiding content. * The component supports a 'header' and 'content' `` for injecting custom HTML. - * Do not set */ interface ModusWcCollapse { /** @@ -546,8 +553,10 @@ export namespace Components { */ "format"?: | 'yyyy-mm-dd' | 'dd-mm-yyyy' + | 'mm-dd-yyyy' | 'yyyy/mm/dd' | 'dd/mm/yyyy' + | 'mm/dd/yyyy' | 'MMM DD, YYYY'; /** * The ID of the input element. @@ -689,6 +698,7 @@ export namespace Components { } /** * File dropzone component that allows users to drag and drop files for upload. + * The component supports a `` called 'dropzone' for adding custom content such as progress indicators or additional instructions within the dropzone area. */ interface ModusWcFileDropzone { /** @@ -744,6 +754,59 @@ export namespace Components { */ "successMessage"?: string; } + /** + * A draggable handle component for resizing adjacent elements + */ + interface ModusWcHandle { + /** + * The color of the button. + */ + "buttonColor"?: | 'primary' + | 'secondary' + | 'tertiary' + | 'warning' + | 'danger'; + /** + * The size of the button. + */ + "buttonSize"?: DaisySize; + /** + * The variant of the button. + */ + "buttonVariant"?: 'borderless' | 'filled' | 'outlined'; + /** + * Custom CSS class to apply to the handle element. + */ + "customClass"?: string; + /** + * The initial split percentage for the left/top panel (1-100). The right/bottom panel gets the remaining percentage. + */ + "defaultSplit"?: number; + /** + * The density/spacing of the handle container (compact: 8px, comfortable: 12px, relaxed: 16px). + */ + "density"?: 'compact' | 'comfortable' | 'relaxed'; + /** + * The left target element to resize (CSS selector or HTMLElement) + */ + "leftTarget"?: string | HTMLElement; + /** + * The orientation of the handle. + */ + "orientation"?: Orientation; + /** + * The right target element to resize (CSS selector or HTMLElement) + */ + "rightTarget"?: string | HTMLElement; + /** + * The size of the handle. + */ + "size"?: 'default' | 'lg' | 'xl' | '2xl'; + /** + * The type of handle to display. + */ + "type"?: 'bar' | 'button'; + } /** * A customizable icon component used to render Modus icons. * This component requires Modus icons to be installed in the host application. See [Modus Icon Usage](/docs/documentation-modus-icon-usage--docs) for steps. @@ -798,7 +861,7 @@ export namespace Components { } /** * A customizable input label component. - * The component supports a `` for injecting additional custom content inside the label, such as icons or formatted text + * The component supports a `` for injecting additional custom content inside the label, such as icons or formatted text. */ interface ModusWcInputLabel { /** @@ -871,7 +934,7 @@ export namespace Components { } /** * A customizable menu component used to display a list of li elements vertically or horizontally. - * The component supports a `` for injecting custom li elements inside the ul + * The component supports a `` for injecting custom li elements inside the ul element. */ interface ModusWcMenu { /** @@ -896,7 +959,8 @@ export namespace Components { "size"?: ModusSize; } /** - * A customizable menu item component used to display the item portion of a menu + * A customizable menu item component used to display the item portion of a menu. + * This component supports a 'start-icon' `` that allows for custom icons to be placed at the beginning of the item. */ interface ModusWcMenuItem { "bordered"?: boolean; @@ -959,7 +1023,7 @@ export namespace Components { } /** * A customizable modal component used to display content in a dialog. - * The component supports a 'header', 'content', and 'footer' for injecting custom HTML + * This component supports 'header', 'content', and 'footer' `` elements for inserting custom HTML. */ interface ModusWcModal { /** @@ -993,8 +1057,7 @@ export namespace Components { } /** * A customizable navbar component used for top level navigation of all Trimble applications. - * The component supports a 'main-menu', 'notifications', and 'apps' `` for injecting custom HTML menus. - * It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML + * The component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML. */ interface ModusWcNavbar { /** @@ -1162,6 +1225,7 @@ export namespace Components { } /** * A customizable panel component used to organize content in a structured layout. + * This component provides 'header', 'body', and 'footer' `` elements for inserting custom HTML. */ interface ModusWcPanel { /** @@ -1183,7 +1247,7 @@ export namespace Components { } /** * A customizable progress component used to show the progress of a task or show the passing of time. - * The radial variant supports slotting in custom HTML to be displayed within the progress circle + * The radial variant supports slotting in custom HTML to be displayed within the progress circle. */ interface ModusWcProgress { /** @@ -1344,6 +1408,7 @@ export namespace Components { } /** * A customizable side navigation component for organizing primary navigation and content areas in an application. + * The component supports a `` for injecting custom content inside the side navigation panel. */ interface ModusWcSideNavigation { /** @@ -1571,6 +1636,7 @@ export namespace Components { } /** * A customizable tabs component used to create groups of tabs. + * The component supports a `` for injecting custom tab content. */ interface ModusWcTabs { /** @@ -1596,6 +1662,7 @@ export namespace Components { } /** * A customizable input component used to create text inputs with types. + * The component supports a `` for injecting additional custom content inside the input, such as icons or formatted text. */ interface ModusWcTextInput { /** @@ -1903,6 +1970,7 @@ export namespace Components { } /** * A customizable toolbar component used to organize content across the entire page. + * This component provides 'start', 'center', and 'end' `` elements for inserting custom HTML. */ interface ModusWcToolbar { /** @@ -1941,6 +2009,62 @@ export namespace Components { */ "tooltipId"?: string; } + /** + * A tree item component that represents a single node in a hierarchical tree structure. + * This component uses the modus-wc-menu-item structure for consistency. + */ + interface ModusWcTreeItem { + /** + * If true, renders a checkbox at the start of the tree item. + */ + "checkbox"?: boolean; + /** + * Custom CSS class to apply to the li element. + */ + "customClass"?: string; + /** + * The disabled state of the tree item. + */ + "disabled"?: boolean; + /** + * Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior. + */ + "hasSubtree"?: boolean; + /** + * The text label displayed for the tree item. + */ + "label": string; + /** + * The selected state of the tree item. + */ + "selected"?: boolean; + /** + * The size of the tree item. + */ + "size"?: 'sm' | 'md' | 'lg'; + /** + * The modus icon name to render at the start of the tree item. + */ + "startIcon"?: string; + /** + * The unique identifying value of the tree item. + */ + "value": string; + } + /** + * A wrapper component that provides the ul element for tree items. + * This component uses the modus-wc-menu structure to wrap tree items in a proper list structure. + */ + interface ModusWcTreeView { + /** + * Custom CSS class to apply to the ul element. + */ + "customClass"?: string; + /** + * Indicates that this list is a nested sublist. + */ + "isSubList"?: boolean; + } /** * A customizable typography component used to render text with different sizes, hierarchy, and weights. * Note: When using heading elements (h1-h6), the default heading CSS styling can be accessed without modifying @@ -2104,6 +2228,10 @@ export interface ModusWcTooltipCustomEvent extends CustomEvent { detail: T; target: HTMLModusWcTooltipElement; } +export interface ModusWcTreeItemCustomEvent extends CustomEvent { + detail: T; + target: HTMLModusWcTreeItemElement; +} export interface ModusWcUtilityPanelCustomEvent extends CustomEvent { detail: T; target: HTMLModusWcUtilityPanelElement; @@ -2117,7 +2245,7 @@ declare global { } /** * A customizable accordion component used for showing and hiding related groups of content. - * The component supports a `` for injecting `` elements. See [Collapse](/docs/components-collapse--docs) docs for additional info. + * The component supports a `` called 'content' for injecting `` elements. See [Collapse](/docs/components-collapse--docs) docs for additional info. */ interface HTMLModusWcAccordionElement extends Components.ModusWcAccordion, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLModusWcAccordionElement, ev: ModusWcAccordionCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -2137,7 +2265,8 @@ declare global { "dismissClick": any; } /** - * A customizable alert component used to inform the user about important events + * A customizable alert component used to inform the user about important events. + * The component supports `` elements for injecting custom content and buttons. */ interface HTMLModusWcAlertElement extends Components.ModusWcAlert, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLModusWcAlertElement, ev: ModusWcAlertCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -2164,6 +2293,7 @@ declare global { } /** * A customizable autocomplete component used to create searchable text inputs. + * The component supports a `` for injecting custom content. */ interface HTMLModusWcAutocompleteElement extends Components.ModusWcAutocomplete, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLModusWcAutocompleteElement, ev: ModusWcAutocompleteCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -2225,7 +2355,7 @@ declare global { } /** * A customizable button component used to create buttons with different sizes, variants, and types. - * The component supports a `` for injecting content within the button, similar to a native HTML button + * The component supports a `` for injecting content within the button, similar to a native HTML button. */ interface HTMLModusWcButtonElement extends Components.ModusWcButton, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLModusWcButtonElement, ev: ModusWcButtonCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -2269,7 +2399,8 @@ declare global { new (): HTMLModusWcButtonGroupElement; }; /** - * A customizable card component used to group and display content in a way that is easily readable + * A customizable card component used to group and display content in a way that is easily readable. + * This component supports multiple `` elements including 'header' for images or custom content, 'title', 'subtitle', a default slot for main content, 'actions' for buttons or interactive elements, and 'footer'. */ interface HTMLModusWcCardElement extends Components.ModusWcCard, HTMLStencilElement { } @@ -2305,6 +2436,7 @@ declare global { } /** * A customizable chip component used to display information in a compact area + * The component supports a `` for injecting custom content such as avatar and icons. */ interface HTMLModusWcChipElement extends Components.ModusWcChip, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLModusWcChipElement, ev: ModusWcChipCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -2326,7 +2458,6 @@ declare global { /** * A customizable collapse component used for showing and hiding content. * The component supports a 'header' and 'content' `` for injecting custom HTML. - * Do not set */ interface HTMLModusWcCollapseElement extends Components.ModusWcCollapse, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLModusWcCollapseElement, ev: ModusWcCollapseCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -2412,6 +2543,7 @@ declare global { } /** * File dropzone component that allows users to drag and drop files for upload. + * The component supports a `` called 'dropzone' for adding custom content such as progress indicators or additional instructions within the dropzone area. */ interface HTMLModusWcFileDropzoneElement extends Components.ModusWcFileDropzone, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLModusWcFileDropzoneElement, ev: ModusWcFileDropzoneCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -2427,6 +2559,15 @@ declare global { prototype: HTMLModusWcFileDropzoneElement; new (): HTMLModusWcFileDropzoneElement; }; + /** + * A draggable handle component for resizing adjacent elements + */ + interface HTMLModusWcHandleElement extends Components.ModusWcHandle, HTMLStencilElement { + } + var HTMLModusWcHandleElement: { + prototype: HTMLModusWcHandleElement; + new (): HTMLModusWcHandleElement; + }; /** * A customizable icon component used to render Modus icons. * This component requires Modus icons to be installed in the host application. See [Modus Icon Usage](/docs/documentation-modus-icon-usage--docs) for steps. @@ -2449,7 +2590,7 @@ declare global { }; /** * A customizable input label component. - * The component supports a `` for injecting additional custom content inside the label, such as icons or formatted text + * The component supports a `` for injecting additional custom content inside the label, such as icons or formatted text. */ interface HTMLModusWcInputLabelElement extends Components.ModusWcInputLabel, HTMLStencilElement { } @@ -2481,7 +2622,7 @@ declare global { } /** * A customizable menu component used to display a list of li elements vertically or horizontally. - * The component supports a `` for injecting custom li elements inside the ul + * The component supports a `` for injecting custom li elements inside the ul element. */ interface HTMLModusWcMenuElement extends Components.ModusWcMenu, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLModusWcMenuElement, ev: ModusWcMenuCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -2504,7 +2645,8 @@ declare global { }; } /** - * A customizable menu item component used to display the item portion of a menu + * A customizable menu item component used to display the item portion of a menu. + * This component supports a 'start-icon' `` that allows for custom icons to be placed at the beginning of the item. */ interface HTMLModusWcMenuItemElement extends Components.ModusWcMenuItem, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLModusWcMenuItemElement, ev: ModusWcMenuItemCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -2522,7 +2664,7 @@ declare global { }; /** * A customizable modal component used to display content in a dialog. - * The component supports a 'header', 'content', and 'footer' for injecting custom HTML + * This component supports 'header', 'content', and 'footer' `` elements for inserting custom HTML. */ interface HTMLModusWcModalElement extends Components.ModusWcModal, HTMLStencilElement { } @@ -2549,8 +2691,7 @@ declare global { } /** * A customizable navbar component used for top level navigation of all Trimble applications. - * The component supports a 'main-menu', 'notifications', and 'apps' `` for injecting custom HTML menus. - * It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML + * The component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML. */ interface HTMLModusWcNavbarElement extends Components.ModusWcNavbar, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLModusWcNavbarElement, ev: ModusWcNavbarCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -2610,6 +2751,7 @@ declare global { }; /** * A customizable panel component used to organize content in a structured layout. + * This component provides 'header', 'body', and 'footer' `` elements for inserting custom HTML. */ interface HTMLModusWcPanelElement extends Components.ModusWcPanel, HTMLStencilElement { } @@ -2619,7 +2761,7 @@ declare global { }; /** * A customizable progress component used to show the progress of a task or show the passing of time. - * The radial variant supports slotting in custom HTML to be displayed within the progress circle + * The radial variant supports slotting in custom HTML to be displayed within the progress circle. */ interface HTMLModusWcProgressElement extends Components.ModusWcProgress, HTMLStencilElement { } @@ -2696,6 +2838,7 @@ declare global { } /** * A customizable side navigation component for organizing primary navigation and content areas in an application. + * The component supports a `` for injecting custom content inside the side navigation panel. */ interface HTMLModusWcSideNavigationElement extends Components.ModusWcSideNavigation, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLModusWcSideNavigationElement, ev: ModusWcSideNavigationCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -2817,6 +2960,7 @@ declare global { } /** * A customizable tabs component used to create groups of tabs. + * The component supports a `` for injecting custom tab content. */ interface HTMLModusWcTabsElement extends Components.ModusWcTabs, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLModusWcTabsElement, ev: ModusWcTabsCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -2840,6 +2984,7 @@ declare global { } /** * A customizable input component used to create text inputs with types. + * The component supports a `` for injecting additional custom content inside the input, such as icons or formatted text. */ interface HTMLModusWcTextInputElement extends Components.ModusWcTextInput, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLModusWcTextInputElement, ev: ModusWcTextInputCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -2938,6 +3083,7 @@ declare global { }; /** * A customizable toolbar component used to organize content across the entire page. + * This component provides 'start', 'center', and 'end' `` elements for inserting custom HTML. */ interface HTMLModusWcToolbarElement extends Components.ModusWcToolbar, HTMLStencilElement { } @@ -2967,6 +3113,40 @@ declare global { prototype: HTMLModusWcTooltipElement; new (): HTMLModusWcTooltipElement; }; + interface HTMLModusWcTreeItemElementEventMap { + "itemSelect": { + value: string; + selected?: boolean; + }; + } + /** + * A tree item component that represents a single node in a hierarchical tree structure. + * This component uses the modus-wc-menu-item structure for consistency. + */ + interface HTMLModusWcTreeItemElement extends Components.ModusWcTreeItem, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLModusWcTreeItemElement, ev: ModusWcTreeItemCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLModusWcTreeItemElement, ev: ModusWcTreeItemCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; + } + var HTMLModusWcTreeItemElement: { + prototype: HTMLModusWcTreeItemElement; + new (): HTMLModusWcTreeItemElement; + }; + /** + * A wrapper component that provides the ul element for tree items. + * This component uses the modus-wc-menu structure to wrap tree items in a proper list structure. + */ + interface HTMLModusWcTreeViewElement extends Components.ModusWcTreeView, HTMLStencilElement { + } + var HTMLModusWcTreeViewElement: { + prototype: HTMLModusWcTreeViewElement; + new (): HTMLModusWcTreeViewElement; + }; /** * A customizable typography component used to render text with different sizes, hierarchy, and weights. * Note: When using heading elements (h1-h6), the default heading CSS styling can be accessed without modifying @@ -3015,6 +3195,7 @@ declare global { "modus-wc-divider": HTMLModusWcDividerElement; "modus-wc-dropdown-menu": HTMLModusWcDropdownMenuElement; "modus-wc-file-dropzone": HTMLModusWcFileDropzoneElement; + "modus-wc-handle": HTMLModusWcHandleElement; "modus-wc-icon": HTMLModusWcIconElement; "modus-wc-input-feedback": HTMLModusWcInputFeedbackElement; "modus-wc-input-label": HTMLModusWcInputLabelElement; @@ -3046,6 +3227,8 @@ declare global { "modus-wc-toast": HTMLModusWcToastElement; "modus-wc-toolbar": HTMLModusWcToolbarElement; "modus-wc-tooltip": HTMLModusWcTooltipElement; + "modus-wc-tree-item": HTMLModusWcTreeItemElement; + "modus-wc-tree-view": HTMLModusWcTreeViewElement; "modus-wc-typography": HTMLModusWcTypographyElement; "modus-wc-utility-panel": HTMLModusWcUtilityPanelElement; } @@ -3053,7 +3236,7 @@ declare global { declare namespace LocalJSX { /** * A customizable accordion component used for showing and hiding related groups of content. - * The component supports a `` for injecting `` elements. See [Collapse](/docs/components-collapse--docs) docs for additional info. + * The component supports a `` called 'content' for injecting `` elements. See [Collapse](/docs/components-collapse--docs) docs for additional info. */ interface ModusWcAccordion { /** @@ -3069,7 +3252,8 @@ declare namespace LocalJSX { }>) => void; } /** - * A customizable alert component used to inform the user about important events + * A customizable alert component used to inform the user about important events. + * The component supports `` elements for injecting custom content and buttons. */ interface ModusWcAlert { /** @@ -3107,6 +3291,7 @@ declare namespace LocalJSX { } /** * A customizable autocomplete component used to create searchable text inputs. + * The component supports a `` for injecting custom content. */ interface ModusWcAutocomplete { /** @@ -3141,6 +3326,10 @@ declare namespace LocalJSX { * Whether the form control is disabled. */ "disabled"?: boolean; + /** + * Feedback state for the input field. + */ + "feedback"?: IInputFeedbackProp; /** * Show the clear button within the input field. */ @@ -3332,7 +3521,7 @@ declare namespace LocalJSX { } /** * A customizable button component used to create buttons with different sizes, variants, and types. - * The component supports a `` for injecting content within the button, similar to a native HTML button + * The component supports a `` for injecting content within the button, similar to a native HTML button. */ interface ModusWcButton { /** @@ -3416,7 +3605,8 @@ declare namespace LocalJSX { "variant"?: 'borderless' | 'filled' | 'outlined'; } /** - * A customizable card component used to group and display content in a way that is easily readable + * A customizable card component used to group and display content in a way that is easily readable. + * This component supports multiple `` elements including 'header' for images or custom content, 'title', 'subtitle', a default slot for main content, 'actions' for buttons or interactive elements, and 'footer'. */ interface ModusWcCard { /** @@ -3499,6 +3689,7 @@ declare namespace LocalJSX { } /** * A customizable chip component used to display information in a compact area + * The component supports a `` for injecting custom content such as avatar and icons. */ interface ModusWcChip { /** @@ -3549,7 +3740,6 @@ declare namespace LocalJSX { /** * A customizable collapse component used for showing and hiding content. * The component supports a 'header' and 'content' `` for injecting custom HTML. - * Do not set */ interface ModusWcCollapse { /** @@ -3617,8 +3807,10 @@ declare namespace LocalJSX { */ "format"?: | 'yyyy-mm-dd' | 'dd-mm-yyyy' + | 'mm-dd-yyyy' | 'yyyy/mm/dd' | 'dd/mm/yyyy' + | 'mm/dd/yyyy' | 'MMM DD, YYYY'; /** * The ID of the input element. @@ -3784,6 +3976,7 @@ declare namespace LocalJSX { } /** * File dropzone component that allows users to drag and drop files for upload. + * The component supports a `` called 'dropzone' for adding custom content such as progress indicators or additional instructions within the dropzone area. */ interface ModusWcFileDropzone { /** @@ -3839,6 +4032,59 @@ declare namespace LocalJSX { */ "successMessage"?: string; } + /** + * A draggable handle component for resizing adjacent elements + */ + interface ModusWcHandle { + /** + * The color of the button. + */ + "buttonColor"?: | 'primary' + | 'secondary' + | 'tertiary' + | 'warning' + | 'danger'; + /** + * The size of the button. + */ + "buttonSize"?: DaisySize; + /** + * The variant of the button. + */ + "buttonVariant"?: 'borderless' | 'filled' | 'outlined'; + /** + * Custom CSS class to apply to the handle element. + */ + "customClass"?: string; + /** + * The initial split percentage for the left/top panel (1-100). The right/bottom panel gets the remaining percentage. + */ + "defaultSplit"?: number; + /** + * The density/spacing of the handle container (compact: 8px, comfortable: 12px, relaxed: 16px). + */ + "density"?: 'compact' | 'comfortable' | 'relaxed'; + /** + * The left target element to resize (CSS selector or HTMLElement) + */ + "leftTarget"?: string | HTMLElement; + /** + * The orientation of the handle. + */ + "orientation"?: Orientation; + /** + * The right target element to resize (CSS selector or HTMLElement) + */ + "rightTarget"?: string | HTMLElement; + /** + * The size of the handle. + */ + "size"?: 'default' | 'lg' | 'xl' | '2xl'; + /** + * The type of handle to display. + */ + "type"?: 'bar' | 'button'; + } /** * A customizable icon component used to render Modus icons. * This component requires Modus icons to be installed in the host application. See [Modus Icon Usage](/docs/documentation-modus-icon-usage--docs) for steps. @@ -3893,7 +4139,7 @@ declare namespace LocalJSX { } /** * A customizable input label component. - * The component supports a `` for injecting additional custom content inside the label, such as icons or formatted text + * The component supports a `` for injecting additional custom content inside the label, such as icons or formatted text. */ interface ModusWcInputLabel { /** @@ -3966,7 +4212,7 @@ declare namespace LocalJSX { } /** * A customizable menu component used to display a list of li elements vertically or horizontally. - * The component supports a `` for injecting custom li elements inside the ul + * The component supports a `` for injecting custom li elements inside the ul element. */ interface ModusWcMenu { /** @@ -3995,7 +4241,8 @@ declare namespace LocalJSX { "size"?: ModusSize; } /** - * A customizable menu item component used to display the item portion of a menu + * A customizable menu item component used to display the item portion of a menu. + * This component supports a 'start-icon' `` that allows for custom icons to be placed at the beginning of the item. */ interface ModusWcMenuItem { "bordered"?: boolean; @@ -4061,7 +4308,7 @@ declare namespace LocalJSX { } /** * A customizable modal component used to display content in a dialog. - * The component supports a 'header', 'content', and 'footer' for injecting custom HTML + * This component supports 'header', 'content', and 'footer' `` elements for inserting custom HTML. */ interface ModusWcModal { /** @@ -4095,8 +4342,7 @@ declare namespace LocalJSX { } /** * A customizable navbar component used for top level navigation of all Trimble applications. - * The component supports a 'main-menu', 'notifications', and 'apps' `` for injecting custom HTML menus. - * It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML + * The component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML. */ interface ModusWcNavbar { /** @@ -4340,6 +4586,7 @@ declare namespace LocalJSX { } /** * A customizable panel component used to organize content in a structured layout. + * This component provides 'header', 'body', and 'footer' `` elements for inserting custom HTML. */ interface ModusWcPanel { /** @@ -4361,7 +4608,7 @@ declare namespace LocalJSX { } /** * A customizable progress component used to show the progress of a task or show the passing of time. - * The radial variant supports slotting in custom HTML to be displayed within the progress circle + * The radial variant supports slotting in custom HTML to be displayed within the progress circle. */ interface ModusWcProgress { /** @@ -4550,6 +4797,7 @@ declare namespace LocalJSX { } /** * A customizable side navigation component for organizing primary navigation and content areas in an application. + * The component supports a `` for injecting custom content inside the side navigation panel. */ interface ModusWcSideNavigation { /** @@ -4843,6 +5091,7 @@ declare namespace LocalJSX { } /** * A customizable tabs component used to create groups of tabs. + * The component supports a `` for injecting custom tab content. */ interface ModusWcTabs { /** @@ -4875,6 +5124,7 @@ declare namespace LocalJSX { } /** * A customizable input component used to create text inputs with types. + * The component supports a `` for injecting additional custom content inside the input, such as icons or formatted text. */ interface ModusWcTextInput { /** @@ -5226,6 +5476,7 @@ declare namespace LocalJSX { } /** * A customizable toolbar component used to organize content across the entire page. + * This component provides 'start', 'center', and 'end' `` elements for inserting custom HTML. */ interface ModusWcToolbar { /** @@ -5268,6 +5519,69 @@ declare namespace LocalJSX { */ "tooltipId"?: string; } + /** + * A tree item component that represents a single node in a hierarchical tree structure. + * This component uses the modus-wc-menu-item structure for consistency. + */ + interface ModusWcTreeItem { + /** + * If true, renders a checkbox at the start of the tree item. + */ + "checkbox"?: boolean; + /** + * Custom CSS class to apply to the li element. + */ + "customClass"?: string; + /** + * The disabled state of the tree item. + */ + "disabled"?: boolean; + /** + * Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior. + */ + "hasSubtree"?: boolean; + /** + * The text label displayed for the tree item. + */ + "label": string; + /** + * Event emitted when a tree item is selected. + */ + "onItemSelect"?: (event: ModusWcTreeItemCustomEvent<{ + value: string; + selected?: boolean; + }>) => void; + /** + * The selected state of the tree item. + */ + "selected"?: boolean; + /** + * The size of the tree item. + */ + "size"?: 'sm' | 'md' | 'lg'; + /** + * The modus icon name to render at the start of the tree item. + */ + "startIcon"?: string; + /** + * The unique identifying value of the tree item. + */ + "value"?: string; + } + /** + * A wrapper component that provides the ul element for tree items. + * This component uses the modus-wc-menu structure to wrap tree items in a proper list structure. + */ + interface ModusWcTreeView { + /** + * Custom CSS class to apply to the ul element. + */ + "customClass"?: string; + /** + * Indicates that this list is a nested sublist. + */ + "isSubList"?: boolean; + } /** * A customizable typography component used to render text with different sizes, hierarchy, and weights. * Note: When using heading elements (h1-h6), the default heading CSS styling can be accessed without modifying @@ -5336,6 +5650,7 @@ declare namespace LocalJSX { "modus-wc-divider": ModusWcDivider; "modus-wc-dropdown-menu": ModusWcDropdownMenu; "modus-wc-file-dropzone": ModusWcFileDropzone; + "modus-wc-handle": ModusWcHandle; "modus-wc-icon": ModusWcIcon; "modus-wc-input-feedback": ModusWcInputFeedback; "modus-wc-input-label": ModusWcInputLabel; @@ -5367,6 +5682,8 @@ declare namespace LocalJSX { "modus-wc-toast": ModusWcToast; "modus-wc-toolbar": ModusWcToolbar; "modus-wc-tooltip": ModusWcTooltip; + "modus-wc-tree-item": ModusWcTreeItem; + "modus-wc-tree-view": ModusWcTreeView; "modus-wc-typography": ModusWcTypography; "modus-wc-utility-panel": ModusWcUtilityPanel; } @@ -5377,15 +5694,17 @@ declare module "@stencil/core" { interface IntrinsicElements { /** * A customizable accordion component used for showing and hiding related groups of content. - * The component supports a `` for injecting `` elements. See [Collapse](/docs/components-collapse--docs) docs for additional info. + * The component supports a `` called 'content' for injecting `` elements. See [Collapse](/docs/components-collapse--docs) docs for additional info. */ "modus-wc-accordion": LocalJSX.ModusWcAccordion & JSXBase.HTMLAttributes; /** - * A customizable alert component used to inform the user about important events + * A customizable alert component used to inform the user about important events. + * The component supports `` elements for injecting custom content and buttons. */ "modus-wc-alert": LocalJSX.ModusWcAlert & JSXBase.HTMLAttributes; /** * A customizable autocomplete component used to create searchable text inputs. + * The component supports a `` for injecting custom content. */ "modus-wc-autocomplete": LocalJSX.ModusWcAutocomplete & JSXBase.HTMLAttributes; /** @@ -5405,7 +5724,7 @@ declare module "@stencil/core" { "modus-wc-breadcrumbs": LocalJSX.ModusWcBreadcrumbs & JSXBase.HTMLAttributes; /** * A customizable button component used to create buttons with different sizes, variants, and types. - * The component supports a `` for injecting content within the button, similar to a native HTML button + * The component supports a `` for injecting content within the button, similar to a native HTML button. */ "modus-wc-button": LocalJSX.ModusWcButton & JSXBase.HTMLAttributes; /** @@ -5414,7 +5733,8 @@ declare module "@stencil/core" { */ "modus-wc-button-group": LocalJSX.ModusWcButtonGroup & JSXBase.HTMLAttributes; /** - * A customizable card component used to group and display content in a way that is easily readable + * A customizable card component used to group and display content in a way that is easily readable. + * This component supports multiple `` elements including 'header' for images or custom content, 'title', 'subtitle', a default slot for main content, 'actions' for buttons or interactive elements, and 'footer'. */ "modus-wc-card": LocalJSX.ModusWcCard & JSXBase.HTMLAttributes; /** @@ -5423,12 +5743,12 @@ declare module "@stencil/core" { "modus-wc-checkbox": LocalJSX.ModusWcCheckbox & JSXBase.HTMLAttributes; /** * A customizable chip component used to display information in a compact area + * The component supports a `` for injecting custom content such as avatar and icons. */ "modus-wc-chip": LocalJSX.ModusWcChip & JSXBase.HTMLAttributes; /** * A customizable collapse component used for showing and hiding content. * The component supports a 'header' and 'content' `` for injecting custom HTML. - * Do not set */ "modus-wc-collapse": LocalJSX.ModusWcCollapse & JSXBase.HTMLAttributes; /** @@ -5452,8 +5772,13 @@ declare module "@stencil/core" { "modus-wc-dropdown-menu": LocalJSX.ModusWcDropdownMenu & JSXBase.HTMLAttributes; /** * File dropzone component that allows users to drag and drop files for upload. + * The component supports a `` called 'dropzone' for adding custom content such as progress indicators or additional instructions within the dropzone area. */ "modus-wc-file-dropzone": LocalJSX.ModusWcFileDropzone & JSXBase.HTMLAttributes; + /** + * A draggable handle component for resizing adjacent elements + */ + "modus-wc-handle": LocalJSX.ModusWcHandle & JSXBase.HTMLAttributes; /** * A customizable icon component used to render Modus icons. * This component requires Modus icons to be installed in the host application. See [Modus Icon Usage](/docs/documentation-modus-icon-usage--docs) for steps. @@ -5466,7 +5791,7 @@ declare module "@stencil/core" { "modus-wc-input-feedback": LocalJSX.ModusWcInputFeedback & JSXBase.HTMLAttributes; /** * A customizable input label component. - * The component supports a `` for injecting additional custom content inside the label, such as icons or formatted text + * The component supports a `` for injecting additional custom content inside the label, such as icons or formatted text. */ "modus-wc-input-label": LocalJSX.ModusWcInputLabel & JSXBase.HTMLAttributes; /** @@ -5480,22 +5805,22 @@ declare module "@stencil/core" { "modus-wc-logo": LocalJSX.ModusWcLogo & JSXBase.HTMLAttributes; /** * A customizable menu component used to display a list of li elements vertically or horizontally. - * The component supports a `` for injecting custom li elements inside the ul + * The component supports a `` for injecting custom li elements inside the ul element. */ "modus-wc-menu": LocalJSX.ModusWcMenu & JSXBase.HTMLAttributes; /** - * A customizable menu item component used to display the item portion of a menu + * A customizable menu item component used to display the item portion of a menu. + * This component supports a 'start-icon' `` that allows for custom icons to be placed at the beginning of the item. */ "modus-wc-menu-item": LocalJSX.ModusWcMenuItem & JSXBase.HTMLAttributes; /** * A customizable modal component used to display content in a dialog. - * The component supports a 'header', 'content', and 'footer' for injecting custom HTML + * This component supports 'header', 'content', and 'footer' `` elements for inserting custom HTML. */ "modus-wc-modal": LocalJSX.ModusWcModal & JSXBase.HTMLAttributes; /** * A customizable navbar component used for top level navigation of all Trimble applications. - * The component supports a 'main-menu', 'notifications', and 'apps' `` for injecting custom HTML menus. - * It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML + * The component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML. */ "modus-wc-navbar": LocalJSX.ModusWcNavbar & JSXBase.HTMLAttributes; /** @@ -5508,11 +5833,12 @@ declare module "@stencil/core" { "modus-wc-pagination": LocalJSX.ModusWcPagination & JSXBase.HTMLAttributes; /** * A customizable panel component used to organize content in a structured layout. + * This component provides 'header', 'body', and 'footer' `` elements for inserting custom HTML. */ "modus-wc-panel": LocalJSX.ModusWcPanel & JSXBase.HTMLAttributes; /** * A customizable progress component used to show the progress of a task or show the passing of time. - * The radial variant supports slotting in custom HTML to be displayed within the progress circle + * The radial variant supports slotting in custom HTML to be displayed within the progress circle. */ "modus-wc-progress": LocalJSX.ModusWcProgress & JSXBase.HTMLAttributes; /** @@ -5529,6 +5855,7 @@ declare module "@stencil/core" { "modus-wc-select": LocalJSX.ModusWcSelect & JSXBase.HTMLAttributes; /** * A customizable side navigation component for organizing primary navigation and content areas in an application. + * The component supports a `` for injecting custom content inside the side navigation panel. */ "modus-wc-side-navigation": LocalJSX.ModusWcSideNavigation & JSXBase.HTMLAttributes; /** @@ -5550,10 +5877,12 @@ declare module "@stencil/core" { "modus-wc-table": LocalJSX.ModusWcTable & JSXBase.HTMLAttributes; /** * A customizable tabs component used to create groups of tabs. + * The component supports a `` for injecting custom tab content. */ "modus-wc-tabs": LocalJSX.ModusWcTabs & JSXBase.HTMLAttributes; /** * A customizable input component used to create text inputs with types. + * The component supports a `` for injecting additional custom content inside the input, such as icons or formatted text. */ "modus-wc-text-input": LocalJSX.ModusWcTextInput & JSXBase.HTMLAttributes; /** @@ -5577,6 +5906,7 @@ declare module "@stencil/core" { "modus-wc-toast": LocalJSX.ModusWcToast & JSXBase.HTMLAttributes; /** * A customizable toolbar component used to organize content across the entire page. + * This component provides 'start', 'center', and 'end' `` elements for inserting custom HTML. */ "modus-wc-toolbar": LocalJSX.ModusWcToolbar & JSXBase.HTMLAttributes; /** @@ -5585,6 +5915,16 @@ declare module "@stencil/core" { * When forceOpen is enabled, the tooltip will remain open and can only be closed by setting forceOpen to false. */ "modus-wc-tooltip": LocalJSX.ModusWcTooltip & JSXBase.HTMLAttributes; + /** + * A tree item component that represents a single node in a hierarchical tree structure. + * This component uses the modus-wc-menu-item structure for consistency. + */ + "modus-wc-tree-item": LocalJSX.ModusWcTreeItem & JSXBase.HTMLAttributes; + /** + * A wrapper component that provides the ul element for tree items. + * This component uses the modus-wc-menu structure to wrap tree items in a proper list structure. + */ + "modus-wc-tree-view": LocalJSX.ModusWcTreeView & JSXBase.HTMLAttributes; /** * A customizable typography component used to render text with different sizes, hierarchy, and weights. * Note: When using heading elements (h1-h6), the default heading CSS styling can be accessed without modifying diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index 20cae86926..75cc6ddb10 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -18,7 +18,7 @@ modus-wc-content-tree { margin-bottom: var(--modus-wc-spacing-md, 1rem); padding-bottom: var(--modus-wc-spacing-sm, 0.75rem); - .modus-wc-icon{ + .modus-wc-icon { cursor: pointer; } } diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts index 923480f724..cfbd87d14e 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts @@ -43,464 +43,60 @@ export const Default: Story = { }, }; -export const UsingSlot: Story = { - render: (args) => { +export const UsingContentTreeItem: Story = { + render: () => { return html` - - - + + - - - - - - + + - - - - - + + + - - - + - - - + - - - + - - - - - - - + + + + + - - - - - - - - - - `; - }, -}; - -export const CustomMenu: Story = { - render: (args) => { - const toggleExpand = (e: Event) => { - const icon = e.target as HTMLElement; - const li = icon.closest('li'); - if (!li) return; - - const iconElement = li.querySelector( - 'modus-wc-icon[name="expand_more"], modus-wc-icon[name="chevron_right"]' - ) as any; - const siblings = Array.from(li.parentElement?.children || []); - const currentIndex = siblings.indexOf(li); - - // Find all child items (next siblings with custom-nested-row class) - const children: HTMLElement[] = []; - for (let i = currentIndex + 1; i < siblings.length; i++) { - const sibling = siblings[i] as HTMLElement; - if (sibling.querySelector('.custom-nested-row')) { - children.push(sibling); - } else { - break; - } - } - - // Toggle icon and children visibility - if (iconElement.getAttribute('name') === 'expand_more') { - iconElement.setAttribute('name', 'chevron_right'); - li.classList.remove('expanded'); - children.forEach((child) => (child.style.display = 'none')); - } else { - iconElement.setAttribute('name', 'expand_more'); - li.classList.add('expanded'); - children.forEach((child) => (child.style.display = '')); - } - }; - - return html` - - - -
  • -
    - - - -
    Parent
    -
    - - - - - - -
    -
    -
  • -
  • -
    - -
    - -
    Child
    -
    - - - - - - -
    -
    -
    -
  • -
  • -
    - -
    - -
    Child
    -
    - - - - - - -
    -
    -
    -
  • -
  • -
    - - - -
    Parent
    -
    - - - - - - -
    -
    -
  • -
    -
    - `; - }, -}; - -export const SingleLevel: Story = { - render: (args) => { - return html` - - - - - - - - - - - - - - + + `; }, diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx index 5905b1a0ce..cc104efcdf 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx @@ -96,7 +96,7 @@ export class ModusWcContentTree { while (parent && parent !== this.el) { if (parent.tagName === 'MODUS-WC-MENU-ITEM') { parent.style.display = ''; - (parent as any).expandSubmenu(); + // (parent as any).expandSubmenu(); } parent = parent.parentElement; } @@ -109,7 +109,7 @@ export class ModusWcContentTree { if (hasMatchingChildren) { (item as HTMLElement).style.display = ''; - (item as any).expandSubmenu(); + // (item as any).expandSubmenu(); } else { (item as HTMLElement).style.display = 'none'; } diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss new file mode 100644 index 0000000000..925de5f557 --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss @@ -0,0 +1,46 @@ +/** +* This component uses modus-wc-menu-item structure and DaisyUI menu classes. +* Only add styles here that should not be applied by Tailwind, Daisy, or the theme. +*/ + +modus-wc-tree-item { + .content-tree-item { + list-style: none; + } + + .modus-wc-tree-item-content { + align-items: center; + display: flex; + gap: var(--modus-wc-spacing-sm, 0.5rem); + } + + .modus-wc-tree-item-labels { + flex: 1; + + .modus-wc-tree-item-label { + display: block; + } + } + + button { + align-items: center; + background: transparent; + border: none; + cursor: pointer; + display: flex; + gap: var(--modus-wc-spacing-sm, 0.5rem); + padding: var(--modus-wc-spacing-xs, 0.25rem) + var(--modus-wc-spacing-sm, 0.5rem); + text-align: start; + width: 100%; + } + + .modus-wc-tree-dropdown { + display: none; + list-style: none; + } + + .modus-wc-tree-dropdown.modus-wc-tree-dropdown-show { + display: block; + } +} diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx new file mode 100644 index 0000000000..d2a5d846d4 --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx @@ -0,0 +1,153 @@ +import { + Component, + Element, + EventEmitter, + h, + Host, + Prop, + State, + Event as StencilEvent, +} from '@stencil/core'; +import { Attributes, inheritAriaAttributes } from '../../utils'; + +/** + * A tree item component that represents a single node in a hierarchical tree structure. + * This component uses the modus-wc-menu-item structure for consistency. + */ +@Component({ + tag: 'modus-wc-tree-item', + styleUrl: 'modus-wc-tree-item.scss', + shadow: false, +}) +export class ModusWcTreeItem { + private inheritedAttributes: Attributes = {}; + + /** Reference to the host element */ + @Element() el!: HTMLElement; + + /** The disabled state of the tree item. */ + @Prop() disabled?: boolean; + + /** The size of the tree item. */ + @Prop() size?: 'sm' | 'md' | 'lg' = 'md'; + + /** If true, renders a checkbox at the start of the tree item. */ + @Prop() checkbox?: boolean = false; + + /** The modus icon name to render at the start of the tree item. */ + @Prop() startIcon?: string; + + /** The text label displayed for the tree item. */ + @Prop() label!: string; + + /** Custom CSS class to apply to the li element. */ + @Prop() customClass?: string = ''; + + /** The selected state of the tree item. */ + @Prop() selected?: boolean; + + /** The unique identifying value of the tree item. */ + @Prop() value: string = ''; + + /** Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior. */ + @Prop() hasSubtree?: boolean; + + /** Internal state to track if subtree is expanded */ + @State() isExpanded: boolean = false; + + /** Event emitted when a tree item is selected. */ + @StencilEvent() itemSelect!: EventEmitter<{ + value: string; + selected?: boolean; + }>; + + componentWillLoad() { + this.inheritedAttributes = inheritAriaAttributes(this.el); + } + + private handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + this.handleItemSelect(); + } + }; + + private getClasses(): string { + const classList: string[] = ['modus-wc-tree-item']; + + if (this.disabled) classList.push('modus-wc-tree-item-disabled'); + if (this.selected && !this.hasSubtree) + classList.push('modus-wc-tree-item-selected'); + if (this.customClass) classList.push(this.customClass); + + return classList.join(' '); + } + + private handleItemSelect = () => { + // For subtree items, handle the toggle + if (this.hasSubtree) { + const submenu = this.el.querySelector( + '.modus-wc-tree-dropdown' + ) as HTMLElement; + const liElement = this.el.querySelector('li'); + + if (submenu && liElement) { + submenu.classList.toggle('modus-wc-tree-dropdown-show'); + const buttonElement = liElement.querySelector('button'); + + // Update internal expanded state and add/remove class + this.isExpanded = submenu.classList.contains( + 'modus-wc-tree-dropdown-show' + ); + + if (this.isExpanded) { + liElement.classList.add('modus-wc-tree-item-expanded'); + if (buttonElement) { + buttonElement.classList.add('modus-wc-tree-dropdown-show'); + } + } else { + liElement.classList.remove('modus-wc-tree-item-expanded'); + if (buttonElement) { + buttonElement.classList.remove('modus-wc-tree-dropdown-show'); + } + } + } + } + // Always emit the event with current selection state + this.itemSelect.emit({ value: this.value, selected: this.selected }); + }; + + render() { + return ( + +
  • +
    + {this.checkbox && ( + + )} + +
    +
    {this.label}
    +
    +
    + +
  • +
    + ); + } +} diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md b/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md new file mode 100644 index 0000000000..52c2bea02f --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md @@ -0,0 +1,51 @@ +# modus-wc-content-tree-item + + + + + + +## Overview + +A tree item component that represents a single node in a hierarchical tree structure. +This component uses the modus-wc-menu-item structure for consistency. + +## Properties + +| Property | Attribute | Description | Type | Default | +| -------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ----------- | +| `checkbox` | `checkbox` | If true, renders a checkbox at the start of the tree item. | `boolean \| undefined` | `false` | +| `customClass` | `custom-class` | Custom CSS class to apply to the li element. | `string \| undefined` | `''` | +| `disabled` | `disabled` | The disabled state of the tree item. | `boolean \| undefined` | `undefined` | +| `hasSubtree` | `has-subtree` | Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior. | `boolean \| undefined` | `undefined` | +| `label` _(required)_ | `label` | The text label displayed for the tree item. | `string` | `undefined` | +| `selected` | `selected` | The selected state of the tree item. | `boolean \| undefined` | `undefined` | +| `size` | `size` | The size of the tree item. | `"lg" \| "md" \| "sm" \| undefined` | `'md'` | +| `startIcon` | `start-icon` | The modus icon name to render at the start of the tree item. | `string \| undefined` | `undefined` | +| `value` | `value` | The unique identifying value of the tree item. | `string` | `''` | + + +## Events + +| Event | Description | Type | +| ------------ | ------------------------------------------- | ------------------------------------------------------------------ | +| `itemSelect` | Event emitted when a tree item is selected. | `CustomEvent<{ value: string; selected?: boolean \| undefined; }>` | + + +## Dependencies + +### Depends on + +- [modus-wc-checkbox](../../modus-wc-checkbox) + +### Graph +```mermaid +graph TD; + modus-wc-tree-item --> modus-wc-checkbox + modus-wc-checkbox --> modus-wc-input-label + style modus-wc-tree-item fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.scss b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.scss new file mode 100644 index 0000000000..dbd0141e8e --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.scss @@ -0,0 +1,17 @@ +/** + * Styles for the tree view wrapper component. + * Uses modus-wc-menu structure with consistent class naming. + * Only add styles here that should not be applied by Tailwind, Daisy, or the theme. + */ + +modus-wc-tree-view.modus-wc-tree-submenu { + display: contents; +} + +modus-wc-tree-view { + .modus-wc-menu { + list-style: none; + margin: 0; + padding: 0; + } +} diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx new file mode 100644 index 0000000000..0dfa1bc535 --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx @@ -0,0 +1,60 @@ +import { Component, Element, h, Host, Prop } from '@stencil/core'; +import { Attributes, inheritAriaAttributes } from '../../utils'; + +/** + * A wrapper component that provides the ul element for tree items. + * This component uses the modus-wc-menu structure to wrap tree items in a proper list structure. + */ +@Component({ + tag: 'modus-wc-tree-view', + styleUrl: 'modus-wc-tree-view.scss', + shadow: false, +}) +export class ModusWcTreeView { + private inheritedAttributes: Attributes = {}; + + /** Reference to the host element */ + @Element() el!: HTMLElement; + + /** Custom CSS class to apply to the ul element. */ + @Prop() customClass?: string = ''; + + /** Indicates that this list is a nested sublist. */ + @Prop() isSubList?: boolean = false; + + componentWillLoad() { + this.inheritedAttributes = inheritAriaAttributes(this.el); + } + + private getClasses(): string { + // For sublists (dropdowns), only add the dropdown class + if (this.isSubList) { + const classList: string[] = ['modus-wc-tree-dropdown']; + if (this.customClass) classList.push(this.customClass); + return classList.join(' '); + } + + // For root tree view, add all standard classes + const classList: string[] = ['modus-wc-menu', 'modus-wc-tree-view']; + + if (this.customClass) { + classList.push(this.customClass); + } + + return classList.join(' '); + } + + render() { + return ( + +
      + +
    +
    + ); + } +} diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/readme.md b/src/components/modus-wc-content-tree/modus-wc-tree-view/readme.md new file mode 100644 index 0000000000..62620137d7 --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/readme.md @@ -0,0 +1,23 @@ +# modus-wc-content-tree-list + + + + + + +## Overview + +A wrapper component that provides the ul element for tree items. +This component uses the modus-wc-menu structure to wrap tree items in a proper list structure. + +## Properties + +| Property | Attribute | Description | Type | Default | +| ------------- | -------------- | --------------------------------------------- | ---------------------- | ------- | +| `customClass` | `custom-class` | Custom CSS class to apply to the ul element. | `string \| undefined` | `''` | +| `isSubList` | `is-sub-list` | Indicates that this list is a nested sublist. | `boolean \| undefined` | `false` | + + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/src/custom-elements.json b/src/custom-elements.json index 2a499f420d..5dd604be41 100644 --- a/src/custom-elements.json +++ b/src/custom-elements.json @@ -8,7 +8,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable accordion component used for showing and hiding related groups of content.\n\nThe component supports a `` called 'content' for injecting `` elements. See [Collapse](/docs/components-collapse--docs) docs for additional info.", + "description": "A customizable accordion component used for showing and hiding related groups of content.\r\n\r\nThe component supports a `` called 'content' for injecting `` elements. See [Collapse](/docs/components-collapse--docs) docs for additional info.", "name": "ModusWcAccordion", "members": [ { @@ -74,7 +74,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable alert component used to inform the user about important events.\n\nThe component supports `` elements for injecting custom content and buttons.", + "description": "A customizable alert component used to inform the user about important events.\r\n\r\nThe component supports `` elements for injecting custom content and buttons.", "name": "ModusWcAlert", "members": [ { @@ -280,7 +280,7 @@ { "name": "props", "type": { - "text": "{\r\n bordered?: boolean;\r\n disabled?: boolean;\r\n readOnly?: boolean;\r\n size?: ModusSize;\r\n}" + "text": "{\r\n bordered?: boolean;\r\n disabled?: boolean;\r\n feedback?: IInputFeedbackProp;\r\n readOnly?: boolean;\r\n size?: ModusSize;\r\n}" } } ] @@ -809,7 +809,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable autocomplete component used to create searchable text inputs.\n\nThe component supports a `` for injecting custom content.", + "description": "A customizable autocomplete component used to create searchable text inputs.\r\n\r\nThe component supports a `` for injecting custom content.", "name": "ModusWcAutocomplete", "members": [ { @@ -966,6 +966,14 @@ "text": "boolean" } }, + { + "name": "feedback", + "fieldName": "feedback", + "description": "Feedback state for the input field.", + "type": { + "text": "IInputFeedbackProp" + } + }, { "name": "include-clear", "fieldName": "includeClear", @@ -1639,7 +1647,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable button component used to create buttons with different sizes, variants, and types.\n\nThe component supports a `` for injecting content within the button, similar to a native HTML button.", + "description": "A customizable button component used to create buttons with different sizes, variants, and types.\r\n\r\nThe component supports a `` for injecting content within the button, similar to a native HTML button.", "name": "ModusWcButton", "members": [ { @@ -1776,7 +1784,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable card component used to group and display content in a way that is easily readable.\n\nThis component supports multiple `` elements including 'header' for images or custom content, 'title', 'subtitle', a default slot for main content, 'actions' for buttons or interactive elements, and 'footer'.", + "description": "A customizable card component used to group and display content in a way that is easily readable.\r\n\r\nThis component supports multiple `` elements including 'header' for images or custom content, 'title', 'subtitle', a default slot for main content, 'actions' for buttons or interactive elements, and 'footer'.", "name": "ModusWcCard", "members": [ { @@ -2029,7 +2037,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable chip component used to display information in a compact area\n\nThe component supports a `` for injecting custom content such as avatar and icons.", + "description": "A customizable chip component used to display information in a compact area\r\n\r\nThe component supports a `` for injecting custom content such as avatar and icons.", "name": "ModusWcChip", "members": [ { @@ -2173,7 +2181,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable collapse component used for showing and hiding content.\n\nThe component supports a 'header' and 'content' `` for injecting custom HTML.", + "description": "A customizable collapse component used for showing and hiding content.\r\n\r\nThe component supports a 'header' and 'content' `` for injecting custom HTML.", "name": "ModusWcCollapse", "members": [ { @@ -2345,6 +2353,214 @@ } ] }, + { + "kind": "javascript-module", + "path": "src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx", + "declarations": [ + { + "kind": "class", + "description": "A tree item component that represents a single node in a hierarchical tree structure.\r\nThis component uses the modus-wc-menu-item structure for consistency.", + "name": "ModusWcTreeItem", + "members": [ + { + "kind": "field", + "name": "el", + "type": { + "text": "HTMLElement" + }, + "description": "Reference to the host element" + }, + { + "kind": "field", + "name": "isExpanded", + "type": { + "text": "boolean" + }, + "default": "false", + "description": "Internal state to track if subtree is expanded" + }, + { + "kind": "method", + "name": "render" + } + ], + "attributes": [ + { + "name": "checkbox", + "fieldName": "checkbox", + "default": "false", + "description": "If true, renders a checkbox at the start of the tree item.", + "type": { + "text": "boolean" + } + }, + { + "name": "custom-class", + "fieldName": "customClass", + "default": "''", + "description": "Custom CSS class to apply to the li element.", + "type": { + "text": "string" + } + }, + { + "name": "disabled", + "fieldName": "disabled", + "description": "The disabled state of the tree item.", + "type": { + "text": "boolean" + } + }, + { + "name": "has-subtree", + "fieldName": "hasSubtree", + "description": "Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior.", + "type": { + "text": "boolean" + } + }, + { + "name": "label", + "fieldName": "label", + "description": "The text label displayed for the tree item.", + "type": { + "text": "string" + } + }, + { + "name": "selected", + "fieldName": "selected", + "description": "The selected state of the tree item.", + "type": { + "text": "boolean" + } + }, + { + "name": "size", + "fieldName": "size", + "default": "'md'", + "description": "The size of the tree item.", + "type": { + "text": "'sm' | 'md' | 'lg'" + } + }, + { + "name": "start-icon", + "fieldName": "startIcon", + "description": "The modus icon name to render at the start of the tree item.", + "type": { + "text": "string" + } + }, + { + "name": "value", + "fieldName": "value", + "default": "''", + "description": "The unique identifying value of the tree item.", + "type": { + "text": "string" + } + } + ], + "tagName": "modus-wc-tree-item", + "events": [ + { + "kind": "field", + "name": "itemSelect", + "type": { + "text": "EventEmitter<{\r\n value: string;\r\n selected?: boolean;\r\n }>" + }, + "description": "Event emitted when a tree item is selected." + } + ], + "customElement": true + } + ], + "exports": [ + { + "kind": "js", + "name": "ModusWcTreeItem", + "declaration": { + "name": "ModusWcTreeItem", + "module": "src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx" + } + }, + { + "kind": "custom-element-definition", + "name": "modus-wc-tree-item", + "declaration": { + "name": "ModusWcTreeItem", + "module": "src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx" + } + } + ] + }, + { + "kind": "javascript-module", + "path": "src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx", + "declarations": [ + { + "kind": "class", + "description": "A wrapper component that provides the ul element for tree items.\r\nThis component uses the modus-wc-menu structure to wrap tree items in a proper list structure.", + "name": "ModusWcTreeView", + "members": [ + { + "kind": "field", + "name": "el", + "type": { + "text": "HTMLElement" + }, + "description": "Reference to the host element" + }, + { + "kind": "method", + "name": "render" + } + ], + "attributes": [ + { + "name": "custom-class", + "fieldName": "customClass", + "default": "''", + "description": "Custom CSS class to apply to the ul element.", + "type": { + "text": "string" + } + }, + { + "name": "is-sub-list", + "fieldName": "isSubList", + "default": "false", + "description": "Indicates that this list is a nested sublist.", + "type": { + "text": "boolean" + } + } + ], + "tagName": "modus-wc-tree-view", + "events": [], + "customElement": true + } + ], + "exports": [ + { + "kind": "js", + "name": "ModusWcTreeView", + "declaration": { + "name": "ModusWcTreeView", + "module": "src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx" + } + }, + { + "kind": "custom-element-definition", + "name": "modus-wc-tree-view", + "declaration": { + "name": "ModusWcTreeView", + "module": "src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx" + } + } + ] + }, { "kind": "javascript-module", "path": "src/components/modus-wc-date/modus-wc-date.tsx", @@ -2488,7 +2704,7 @@ "default": "'dd-mm-yyyy'", "description": "The date format for display and input.", "type": { - "text": "| 'yyyy-mm-dd'\n | 'dd-mm-yyyy'\n | 'mm-dd-yyyy'\n | 'yyyy/mm/dd'\n | 'dd/mm/yyyy'\n | 'mm/dd/yyyy'\n | 'MMM DD, YYYY'" + "text": "| 'yyyy-mm-dd'\r\n | 'dd-mm-yyyy'\r\n | 'mm-dd-yyyy'\r\n | 'yyyy/mm/dd'\r\n | 'dd/mm/yyyy'\r\n | 'mm/dd/yyyy'\r\n | 'MMM DD, YYYY'" } }, { @@ -2988,7 +3204,7 @@ "declarations": [ { "kind": "class", - "description": "File dropzone component that allows users to drag and drop files for upload.\n\nThe component supports a `` called 'dropzone' for adding custom content such as progress indicators or additional instructions within the dropzone area.", + "description": "File dropzone component that allows users to drag and drop files for upload.\r\n\r\nThe component supports a `` called 'dropzone' for adding custom content such as progress indicators or additional instructions within the dropzone area.", "name": "ModusWcFileDropzone", "members": [ { @@ -3183,7 +3399,7 @@ "default": "'tertiary'", "description": "The color of the button.", "type": { - "text": "| 'primary'\n | 'secondary'\n | 'tertiary'\n | 'warning'\n | 'danger'" + "text": "| 'primary'\r\n | 'secondary'\r\n | 'tertiary'\r\n | 'warning'\r\n | 'danger'" } }, { @@ -3488,7 +3704,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable input label component.\n\nThe component supports a `` for injecting additional custom content inside the label, such as icons or formatted text.", + "description": "A customizable input label component.\r\n\r\nThe component supports a `` for injecting additional custom content inside the label, such as icons or formatted text.", "name": "ModusWcInputLabel", "members": [ { @@ -3753,7 +3969,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable menu item component used to display the item portion of a menu.\n\nThis component supports a 'start-icon' `` that allows for custom icons to be placed at the beginning of the item.", + "description": "A customizable menu item component used to display the item portion of a menu.\r\n\r\nThis component supports a 'start-icon' `` that allows for custom icons to be placed at the beginning of the item.", "name": "ModusWcMenuItem", "members": [ { @@ -3774,15 +3990,6 @@ }, "description": "Reference to the host element" }, - { - "kind": "method", - "name": "expandSubmenu", - "return": { - "type": { - "text": "Promise" - } - } - }, { "kind": "field", "name": "isExpanded", @@ -3846,14 +4053,6 @@ "text": "boolean" } }, - { - "name": "indeterminate", - "fieldName": "indeterminate", - "description": "The indeterminate state of the checkbox.", - "type": { - "text": "boolean" - } - }, { "name": "label", "fieldName": "label", @@ -3962,7 +4161,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable menu component used to display a list of li elements vertically or horizontally.\n\nThe component supports a `` for injecting custom li elements inside the ul element.", + "description": "A customizable menu component used to display a list of li elements vertically or horizontally.\r\n\r\nThe component supports a `` for injecting custom li elements inside the ul element.", "name": "ModusWcMenu", "members": [ { @@ -4062,7 +4261,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable modal component used to display content in a dialog.\n\nThis component supports 'header', 'content', and 'footer' `` elements for inserting custom HTML.", + "description": "A customizable modal component used to display content in a dialog.\r\n\r\nThis component supports 'header', 'content', and 'footer' `` elements for inserting custom HTML.", "name": "ModusWcModal", "members": [ { @@ -4172,7 +4371,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable navbar component used for top level navigation of all Trimble applications.\n\nThe component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML.", + "description": "A customizable navbar component used for top level navigation of all Trimble applications.\r\n\r\nThe component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML.", "name": "ModusWcNavbar", "members": [ { @@ -4814,7 +5013,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable panel component used to organize content in a structured layout.\n\nThis component provides 'header', 'body', and 'footer' `` elements for inserting custom HTML.", + "description": "A customizable panel component used to organize content in a structured layout.\r\n\r\nThis component provides 'header', 'body', and 'footer' `` elements for inserting custom HTML.", "name": "ModusWcPanel", "members": [ { @@ -4898,7 +5097,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable progress component used to show the progress of a task or show the passing of time.\n\nThe radial variant supports slotting in custom HTML to be displayed within the progress circle.", + "description": "A customizable progress component used to show the progress of a task or show the passing of time.\r\n\r\nThe radial variant supports slotting in custom HTML to be displayed within the progress circle.", "name": "ModusWcProgress", "members": [ { @@ -5453,7 +5652,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable side navigation component for organizing primary navigation and content areas in an application.\n\nThe component supports a `` for injecting custom content inside the side navigation panel.", + "description": "A customizable side navigation component for organizing primary navigation and content areas in an application.\r\n\r\nThe component supports a `` for injecting custom content inside the side navigation panel.", "name": "ModusWcSideNavigation", "members": [ { @@ -6412,7 +6611,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable tabs component used to create groups of tabs.\n\nThe component supports a `` for injecting custom tab content.", + "description": "A customizable tabs component used to create groups of tabs.\r\n\r\nThe component supports a `` for injecting custom tab content.", "name": "ModusWcTabs", "members": [ { @@ -6511,7 +6710,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable input component used to create text inputs with types.\n\nThe component supports a `` for injecting additional custom content inside the input, such as icons or formatted text.", + "description": "A customizable input component used to create text inputs with types.\r\n\r\nThe component supports a `` for injecting additional custom content inside the input, such as icons or formatted text.", "name": "ModusWcTextInput", "members": [ { @@ -7419,7 +7618,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable toolbar component used to organize content across the entire page.\n\nThis component provides 'start', 'center', and 'end' `` elements for inserting custom HTML.", + "description": "A customizable toolbar component used to organize content across the entire page.\r\n\r\nThis component provides 'start', 'center', and 'end' `` elements for inserting custom HTML.", "name": "ModusWcToolbar", "members": [ { From 72e136284b987c271b0a98101d091a019f70357c Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Mon, 16 Feb 2026 19:07:05 +0530 Subject: [PATCH 09/39] 662 - add selection state to item and css fixes --- src/components.d.ts | 30 ++--- .../modus-wc-content-tree.stories.ts | 63 +++++++++-- .../modus-wc-tree-item.scss | 33 +++++- .../modus-wc-tree-item.tailwind.ts | 27 +++++ .../modus-wc-tree-item/modus-wc-tree-item.tsx | 107 +++++++++--------- .../modus-wc-tree-item/readme.md | 30 ++--- .../modus-wc-tree-view/modus-wc-tree-view.tsx | 20 ++-- src/custom-elements.json | 40 ++++--- 8 files changed, 225 insertions(+), 125 deletions(-) create mode 100644 src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tailwind.ts diff --git a/src/components.d.ts b/src/components.d.ts index ebb1b2759f..fe342a631a 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -2011,7 +2011,6 @@ export namespace Components { } /** * A tree item component that represents a single node in a hierarchical tree structure. - * This component uses the modus-wc-menu-item structure for consistency. */ interface ModusWcTreeItem { /** @@ -2026,6 +2025,10 @@ export namespace Components { * The disabled state of the tree item. */ "disabled"?: boolean; + /** + * If true, renders a drag handle icon at the start of the tree item. + */ + "dragHandle"?: boolean; /** * Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior. */ @@ -2038,14 +2041,6 @@ export namespace Components { * The selected state of the tree item. */ "selected"?: boolean; - /** - * The size of the tree item. - */ - "size"?: 'sm' | 'md' | 'lg'; - /** - * The modus icon name to render at the start of the tree item. - */ - "startIcon"?: string; /** * The unique identifying value of the tree item. */ @@ -3116,12 +3111,10 @@ declare global { interface HTMLModusWcTreeItemElementEventMap { "itemSelect": { value: string; - selected?: boolean; }; } /** * A tree item component that represents a single node in a hierarchical tree structure. - * This component uses the modus-wc-menu-item structure for consistency. */ interface HTMLModusWcTreeItemElement extends Components.ModusWcTreeItem, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLModusWcTreeItemElement, ev: ModusWcTreeItemCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -5521,7 +5514,6 @@ declare namespace LocalJSX { } /** * A tree item component that represents a single node in a hierarchical tree structure. - * This component uses the modus-wc-menu-item structure for consistency. */ interface ModusWcTreeItem { /** @@ -5536,6 +5528,10 @@ declare namespace LocalJSX { * The disabled state of the tree item. */ "disabled"?: boolean; + /** + * If true, renders a drag handle icon at the start of the tree item. + */ + "dragHandle"?: boolean; /** * Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior. */ @@ -5549,20 +5545,11 @@ declare namespace LocalJSX { */ "onItemSelect"?: (event: ModusWcTreeItemCustomEvent<{ value: string; - selected?: boolean; }>) => void; /** * The selected state of the tree item. */ "selected"?: boolean; - /** - * The size of the tree item. - */ - "size"?: 'sm' | 'md' | 'lg'; - /** - * The modus icon name to render at the start of the tree item. - */ - "startIcon"?: string; /** * The unique identifying value of the tree item. */ @@ -5917,7 +5904,6 @@ declare module "@stencil/core" { "modus-wc-tooltip": LocalJSX.ModusWcTooltip & JSXBase.HTMLAttributes; /** * A tree item component that represents a single node in a hierarchical tree structure. - * This component uses the modus-wc-menu-item structure for consistency. */ "modus-wc-tree-item": LocalJSX.ModusWcTreeItem & JSXBase.HTMLAttributes; /** diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts index cfbd87d14e..90177d37f0 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts @@ -49,52 +49,101 @@ export const UsingContentTreeItem: Story = { + + > + + + > + + + + > + + + + > + + + diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss index 925de5f557..11373aa3e7 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss @@ -8,10 +8,21 @@ modus-wc-tree-item { list-style: none; } - .modus-wc-tree-item-content { + .modus-wc-tree-item-active { + background-color: var(--modus-wc-color-blue-pale); + color: var(--modus-wc-color-primary); + border-radius: unset; + } + + .modus-wc-tree-content { align-items: center; display: flex; gap: var(--modus-wc-spacing-sm, 0.5rem); + + .modus-wc-tree-drag-handle { + position: absolute; + left: 0; + } } .modus-wc-tree-item-labels { @@ -38,9 +49,25 @@ modus-wc-tree-item { .modus-wc-tree-dropdown { display: none; list-style: none; + + &.modus-wc-tree-dropdown-show { + display: block; + } } +} - .modus-wc-tree-dropdown.modus-wc-tree-dropdown-show { - display: block; +[data-theme='modus-classic-dark'], +[data-theme='modus-modern-dark'], +[data-theme='connect-dark'] { + modus-wc-tree-item { + .modus-wc-tree-content { + &.modus-wc-tree-item-active { + background-color: color-mix( + in sRGB, + var(--modus-wc-color-primary) 30%, + transparent + ); + } + } } } diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tailwind.ts b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tailwind.ts new file mode 100644 index 0000000000..ca3e10c19d --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tailwind.ts @@ -0,0 +1,27 @@ +import { ModusSize } from '../../types'; + +export const convertPropsToClasses = ({ + disabled, + selected, + size, +}: { + disabled?: boolean; + selected?: boolean; + size?: ModusSize; +}): string => { + let classes = ''; + + if (disabled) { + classes = `${classes} modus-wc-tree-item-disabled`; + } + + if (selected) { + classes = `${classes} modus-wc-tree-item-selected`; + } + + if (size) { + classes = `${classes} modus-wc-tree-item-${size}`; + } + + return classes.trim(); +}; diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx index d2a5d846d4..59d68d6ce5 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx @@ -9,10 +9,10 @@ import { Event as StencilEvent, } from '@stencil/core'; import { Attributes, inheritAriaAttributes } from '../../utils'; +import { convertPropsToClasses } from './modus-wc-tree-item.tailwind'; /** * A tree item component that represents a single node in a hierarchical tree structure. - * This component uses the modus-wc-menu-item structure for consistency. */ @Component({ tag: 'modus-wc-tree-item', @@ -28,14 +28,11 @@ export class ModusWcTreeItem { /** The disabled state of the tree item. */ @Prop() disabled?: boolean; - /** The size of the tree item. */ - @Prop() size?: 'sm' | 'md' | 'lg' = 'md'; - /** If true, renders a checkbox at the start of the tree item. */ @Prop() checkbox?: boolean = false; - /** The modus icon name to render at the start of the tree item. */ - @Prop() startIcon?: string; + /** If true, renders a drag handle icon at the start of the tree item. */ + @Prop() dragHandle?: boolean = false; /** The text label displayed for the tree item. */ @Prop() label!: string; @@ -44,7 +41,7 @@ export class ModusWcTreeItem { @Prop() customClass?: string = ''; /** The selected state of the tree item. */ - @Prop() selected?: boolean; + @Prop({ mutable: true, reflect: true }) selected?: boolean; /** The unique identifying value of the tree item. */ @Prop() value: string = ''; @@ -58,63 +55,53 @@ export class ModusWcTreeItem { /** Event emitted when a tree item is selected. */ @StencilEvent() itemSelect!: EventEmitter<{ value: string; - selected?: boolean; }>; componentWillLoad() { this.inheritedAttributes = inheritAriaAttributes(this.el); } - private handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - this.handleItemSelect(); - } - }; - private getClasses(): string { const classList: string[] = ['modus-wc-tree-item']; - if (this.disabled) classList.push('modus-wc-tree-item-disabled'); - if (this.selected && !this.hasSubtree) - classList.push('modus-wc-tree-item-selected'); + const propClasses = convertPropsToClasses({ + disabled: this.disabled, + selected: this.selected, + }); + + if (propClasses) classList.push(propClasses); if (this.customClass) classList.push(this.customClass); return classList.join(' '); } - private handleItemSelect = () => { - // For subtree items, handle the toggle - if (this.hasSubtree) { - const submenu = this.el.querySelector( - '.modus-wc-tree-dropdown' - ) as HTMLElement; - const liElement = this.el.querySelector('li'); - - if (submenu && liElement) { - submenu.classList.toggle('modus-wc-tree-dropdown-show'); - const buttonElement = liElement.querySelector('button'); - - // Update internal expanded state and add/remove class - this.isExpanded = submenu.classList.contains( - 'modus-wc-tree-dropdown-show' - ); - - if (this.isExpanded) { - liElement.classList.add('modus-wc-tree-item-expanded'); - if (buttonElement) { - buttonElement.classList.add('modus-wc-tree-dropdown-show'); - } - } else { - liElement.classList.remove('modus-wc-tree-item-expanded'); - if (buttonElement) { - buttonElement.classList.remove('modus-wc-tree-dropdown-show'); - } - } - } + private handleToggleClick = (event: MouseEvent) => { + event.stopPropagation(); + if (!this.hasSubtree) return; + + this.isExpanded = !this.isExpanded; + + const submenu = this.el.querySelector( + '.modus-wc-tree-dropdown' + ) as HTMLElement; + + submenu?.classList.toggle('modus-wc-tree-dropdown-show'); + }; + + private handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + this.handleEmittedSelect(); } - // Always emit the event with current selection state - this.itemSelect.emit({ value: this.value, selected: this.selected }); + }; + + private handleItemSelect = (event: MouseEvent) => { + event.stopPropagation(); + this.handleEmittedSelect(); + }; + + private handleEmittedSelect = () => { + this.itemSelect.emit({ value: this.value }); }; render() { @@ -122,8 +109,8 @@ export class ModusWcTreeItem {
  • -
    +
    + {this.dragHandle && ( + + )} + {this.hasSubtree && ( + + )} {this.checkbox && ( )} diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md b/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md index 52c2bea02f..cc24773c0a 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md @@ -8,39 +8,39 @@ ## Overview A tree item component that represents a single node in a hierarchical tree structure. -This component uses the modus-wc-menu-item structure for consistency. ## Properties -| Property | Attribute | Description | Type | Default | -| -------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ----------- | -| `checkbox` | `checkbox` | If true, renders a checkbox at the start of the tree item. | `boolean \| undefined` | `false` | -| `customClass` | `custom-class` | Custom CSS class to apply to the li element. | `string \| undefined` | `''` | -| `disabled` | `disabled` | The disabled state of the tree item. | `boolean \| undefined` | `undefined` | -| `hasSubtree` | `has-subtree` | Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior. | `boolean \| undefined` | `undefined` | -| `label` _(required)_ | `label` | The text label displayed for the tree item. | `string` | `undefined` | -| `selected` | `selected` | The selected state of the tree item. | `boolean \| undefined` | `undefined` | -| `size` | `size` | The size of the tree item. | `"lg" \| "md" \| "sm" \| undefined` | `'md'` | -| `startIcon` | `start-icon` | The modus icon name to render at the start of the tree item. | `string \| undefined` | `undefined` | -| `value` | `value` | The unique identifying value of the tree item. | `string` | `''` | +| Property | Attribute | Description | Type | Default | +| -------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------- | ----------- | +| `checkbox` | `checkbox` | If true, renders a checkbox at the start of the tree item. | `boolean \| undefined` | `false` | +| `customClass` | `custom-class` | Custom CSS class to apply to the li element. | `string \| undefined` | `''` | +| `disabled` | `disabled` | The disabled state of the tree item. | `boolean \| undefined` | `undefined` | +| `dragHandle` | `drag-handle` | If true, renders a drag handle icon at the start of the tree item. | `boolean \| undefined` | `false` | +| `hasSubtree` | `has-subtree` | Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior. | `boolean \| undefined` | `undefined` | +| `label` _(required)_ | `label` | The text label displayed for the tree item. | `string` | `undefined` | +| `selected` | `selected` | The selected state of the tree item. | `boolean \| undefined` | `undefined` | +| `value` | `value` | The unique identifying value of the tree item. | `string` | `''` | ## Events -| Event | Description | Type | -| ------------ | ------------------------------------------- | ------------------------------------------------------------------ | -| `itemSelect` | Event emitted when a tree item is selected. | `CustomEvent<{ value: string; selected?: boolean \| undefined; }>` | +| Event | Description | Type | +| ------------ | ------------------------------------------- | --------------------------------- | +| `itemSelect` | Event emitted when a tree item is selected. | `CustomEvent<{ value: string; }>` | ## Dependencies ### Depends on +- [modus-wc-icon](../../modus-wc-icon) - [modus-wc-checkbox](../../modus-wc-checkbox) ### Graph ```mermaid graph TD; + modus-wc-tree-item --> modus-wc-icon modus-wc-tree-item --> modus-wc-checkbox modus-wc-checkbox --> modus-wc-input-label style modus-wc-tree-item fill:#f9f,stroke:#333,stroke-width:4px diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx index 0dfa1bc535..67bfdcdb2f 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx @@ -1,4 +1,4 @@ -import { Component, Element, h, Host, Prop } from '@stencil/core'; +import { Component, Element, h, Host, Listen, Prop } from '@stencil/core'; import { Attributes, inheritAriaAttributes } from '../../utils'; /** @@ -26,21 +26,25 @@ export class ModusWcTreeView { this.inheritedAttributes = inheritAriaAttributes(this.el); } + @Listen('itemSelect') + handleItemSelect(event: CustomEvent<{ value: string }>) { + const target = event.target as HTMLElement; + + const allItems = this.el.querySelectorAll('modus-wc-tree-item'); + allItems.forEach((item) => { + (item as HTMLModusWcTreeItemElement).selected = item === target; + }); + } + private getClasses(): string { - // For sublists (dropdowns), only add the dropdown class if (this.isSubList) { const classList: string[] = ['modus-wc-tree-dropdown']; if (this.customClass) classList.push(this.customClass); return classList.join(' '); } - // For root tree view, add all standard classes const classList: string[] = ['modus-wc-menu', 'modus-wc-tree-view']; - - if (this.customClass) { - classList.push(this.customClass); - } - + if (this.customClass) classList.push(this.customClass); return classList.join(' '); } diff --git a/src/custom-elements.json b/src/custom-elements.json index 5dd604be41..06f8a27a7b 100644 --- a/src/custom-elements.json +++ b/src/custom-elements.json @@ -2359,7 +2359,7 @@ "declarations": [ { "kind": "class", - "description": "A tree item component that represents a single node in a hierarchical tree structure.\r\nThis component uses the modus-wc-menu-item structure for consistency.", + "description": "A tree item component that represents a single node in a hierarchical tree structure.", "name": "ModusWcTreeItem", "members": [ { @@ -2411,6 +2411,15 @@ "text": "boolean" } }, + { + "name": "drag-handle", + "fieldName": "dragHandle", + "default": "false", + "description": "If true, renders a drag handle icon at the start of the tree item.", + "type": { + "text": "boolean" + } + }, { "name": "has-subtree", "fieldName": "hasSubtree", @@ -2435,23 +2444,6 @@ "text": "boolean" } }, - { - "name": "size", - "fieldName": "size", - "default": "'md'", - "description": "The size of the tree item.", - "type": { - "text": "'sm' | 'md' | 'lg'" - } - }, - { - "name": "start-icon", - "fieldName": "startIcon", - "description": "The modus icon name to render at the start of the tree item.", - "type": { - "text": "string" - } - }, { "name": "value", "fieldName": "value", @@ -2512,6 +2504,18 @@ }, "description": "Reference to the host element" }, + { + "kind": "method", + "name": "handleItemSelect", + "parameters": [ + { + "name": "event", + "type": { + "text": "CustomEvent<{ value: string; selected?: boolean }>" + } + } + ] + }, { "kind": "method", "name": "render" From c770eb065eb0b131dd41d00b95dc157ec6ee778b Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Tue, 17 Feb 2026 18:09:53 +0530 Subject: [PATCH 10/39] 662 - add row actions, 1.0 & 2.0 features --- src/components.d.ts | 87 +++++++ .../modus-wc-content-tree.scss | 2 +- .../modus-wc-content-tree.stories.ts | 132 +++++++++- .../modus-wc-content-tree.tsx | 85 ++++--- .../modus-wc-tree-actions.scss | 73 ++++++ .../modus-wc-tree-actions.tsx | 232 ++++++++++++++++++ .../modus-wc-tree-actions/readme.md | 46 ++++ .../modus-wc-tree-item.scss | 21 +- .../modus-wc-tree-item/modus-wc-tree-item.tsx | 51 ++++ .../modus-wc-tree-item/readme.md | 49 +++- .../modus-wc-content-tree/readme.md | 4 +- src/custom-elements.json | 144 ++++++++++- 12 files changed, 867 insertions(+), 59 deletions(-) create mode 100644 src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss create mode 100644 src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx create mode 100644 src/components/modus-wc-content-tree/modus-wc-tree-actions/readme.md diff --git a/src/components.d.ts b/src/components.d.ts index fe342a631a..52d325feb6 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -21,6 +21,8 @@ import { SortingState } from "@tanstack/table-core"; import { ITab } from "./components/modus-wc-tabs/modus-wc-tabs"; import { IThemeConfig } from "./providers/theme/theme.types"; import { ToastPosition } from "./components/modus-wc-toast/modus-wc-toast"; +import { ModusTreeItemActions } from "./components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions"; +import { ModusTreeItemActions as ModusTreeItemActions1 } from "./components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions"; import { TypographyHierarchy, TypographySize, TypographyWeight } from "./components/modus-wc-typography/modus-wc-typography"; export { AutocompleteTypes, DaisySize, Density, IAutocompleteItem, IAutocompleteNoResults, IInputFeedbackProp, ModusSize, Orientation, PopoverPlacement, TextFieldTypes, WeekStartDay } from "./components/types"; export { IBreadcrumb } from "./components/modus-wc-breadcrumbs/modus-wc-breadcrumbs"; @@ -38,6 +40,8 @@ export { SortingState } from "@tanstack/table-core"; export { ITab } from "./components/modus-wc-tabs/modus-wc-tabs"; export { IThemeConfig } from "./providers/theme/theme.types"; export { ToastPosition } from "./components/modus-wc-toast/modus-wc-toast"; +export { ModusTreeItemActions } from "./components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions"; +export { ModusTreeItemActions as ModusTreeItemActions1 } from "./components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions"; export { TypographyHierarchy, TypographySize, TypographyWeight } from "./components/modus-wc-typography/modus-wc-typography"; export namespace Components { /** @@ -2009,6 +2013,16 @@ export namespace Components { */ "tooltipId"?: string; } + interface ModusWcTreeActions { + /** + * List of actions to display + */ + "actions"?: ModusTreeItemActions[]; + /** + * The size of the action buttons and icons. + */ + "size": 'xs' | 'sm' | 'md'; + } /** * A tree item component that represents a single node in a hierarchical tree structure. */ @@ -2017,6 +2031,10 @@ export namespace Components { * If true, renders a checkbox at the start of the tree item. */ "checkbox"?: boolean; + /** + * Public method to collapse the subtree if it's expanded + */ + "collapseSubTree": () => Promise; /** * Custom CSS class to apply to the li element. */ @@ -2029,6 +2047,10 @@ export namespace Components { * If true, renders a drag handle icon at the start of the tree item. */ "dragHandle"?: boolean; + /** + * Public method to expand the subtree if it's collapsed + */ + "expandSubTree": () => Promise; /** * Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior. */ @@ -2041,6 +2063,14 @@ export namespace Components { * The selected state of the tree item. */ "selected"?: boolean; + /** + * The size of the tree item icons and actions. + */ + "size": 'xs' | 'sm' | 'md'; + /** + * Actions to display for this tree item. + */ + "treeItemActions"?: ModusTreeItemActions1[]; /** * The unique identifying value of the tree item. */ @@ -2223,6 +2253,10 @@ export interface ModusWcTooltipCustomEvent extends CustomEvent { detail: T; target: HTMLModusWcTooltipElement; } +export interface ModusWcTreeActionsCustomEvent extends CustomEvent { + detail: T; + target: HTMLModusWcTreeActionsElement; +} export interface ModusWcTreeItemCustomEvent extends CustomEvent { detail: T; target: HTMLModusWcTreeItemElement; @@ -3108,6 +3142,27 @@ declare global { prototype: HTMLModusWcTooltipElement; new (): HTMLModusWcTooltipElement; }; + interface HTMLModusWcTreeActionsElementEventMap { + "dropdownOpened": HTMLElement; + "treeActionClick": { + actionId: string; + actionName: string; + }; + } + interface HTMLModusWcTreeActionsElement extends Components.ModusWcTreeActions, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLModusWcTreeActionsElement, ev: ModusWcTreeActionsCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLModusWcTreeActionsElement, ev: ModusWcTreeActionsCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; + } + var HTMLModusWcTreeActionsElement: { + prototype: HTMLModusWcTreeActionsElement; + new (): HTMLModusWcTreeActionsElement; + }; interface HTMLModusWcTreeItemElementEventMap { "itemSelect": { value: string; @@ -3220,6 +3275,7 @@ declare global { "modus-wc-toast": HTMLModusWcToastElement; "modus-wc-toolbar": HTMLModusWcToolbarElement; "modus-wc-tooltip": HTMLModusWcTooltipElement; + "modus-wc-tree-actions": HTMLModusWcTreeActionsElement; "modus-wc-tree-item": HTMLModusWcTreeItemElement; "modus-wc-tree-view": HTMLModusWcTreeViewElement; "modus-wc-typography": HTMLModusWcTypographyElement; @@ -5512,6 +5568,27 @@ declare namespace LocalJSX { */ "tooltipId"?: string; } + interface ModusWcTreeActions { + /** + * List of actions to display + */ + "actions"?: ModusTreeItemActions[]; + /** + * Event emitted when a dropdown is opened + */ + "onDropdownOpened"?: (event: ModusWcTreeActionsCustomEvent) => void; + /** + * Event emitted when an action is clicked + */ + "onTreeActionClick"?: (event: ModusWcTreeActionsCustomEvent<{ + actionId: string; + actionName: string; + }>) => void; + /** + * The size of the action buttons and icons. + */ + "size"?: 'xs' | 'sm' | 'md'; + } /** * A tree item component that represents a single node in a hierarchical tree structure. */ @@ -5550,6 +5627,14 @@ declare namespace LocalJSX { * The selected state of the tree item. */ "selected"?: boolean; + /** + * The size of the tree item icons and actions. + */ + "size"?: 'xs' | 'sm' | 'md'; + /** + * Actions to display for this tree item. + */ + "treeItemActions"?: ModusTreeItemActions1[]; /** * The unique identifying value of the tree item. */ @@ -5669,6 +5754,7 @@ declare namespace LocalJSX { "modus-wc-toast": ModusWcToast; "modus-wc-toolbar": ModusWcToolbar; "modus-wc-tooltip": ModusWcTooltip; + "modus-wc-tree-actions": ModusWcTreeActions; "modus-wc-tree-item": ModusWcTreeItem; "modus-wc-tree-view": ModusWcTreeView; "modus-wc-typography": ModusWcTypography; @@ -5902,6 +5988,7 @@ declare module "@stencil/core" { * When forceOpen is enabled, the tooltip will remain open and can only be closed by setting forceOpen to false. */ "modus-wc-tooltip": LocalJSX.ModusWcTooltip & JSXBase.HTMLAttributes; + "modus-wc-tree-actions": LocalJSX.ModusWcTreeActions & JSXBase.HTMLAttributes; /** * A tree item component that represents a single node in a hierarchical tree structure. */ diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index 75cc6ddb10..9dc5d4edfe 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -15,7 +15,7 @@ modus-wc-content-tree { display: flex; gap: var(--modus-wc-spacing-xs, 0.5rem); justify-content: flex-end; - margin-bottom: var(--modus-wc-spacing-md, 1rem); + margin-top: var(--modus-wc-spacing-md, 1rem); padding-bottom: var(--modus-wc-spacing-sm, 0.75rem); .modus-wc-icon { diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts index 90177d37f0..245c4ee847 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts @@ -22,7 +22,7 @@ const meta: Meta = { decorators: [withActions], parameters: { actions: { - handles: ['itemSelect'], + handles: ['itemSelect', 'treeActionClick'], }, }, }; @@ -45,14 +45,48 @@ export const Default: Story = { export const UsingContentTreeItem: Story = { render: () => { + const actions = [ + { + id: 'view', + label: 'View', + icon: 'visibility_on', + ariaLabel: 'View item', + }, + { + id: 'edit', + label: 'Edit', + icon: 'pencil', + ariaLabel: 'Edit item', + }, + { + id: 'delete', + label: 'Delete', + icon: 'delete', + ariaLabel: 'Delete item', + }, + ]; + return html` + + + + + + `; + }, +}; + +export const WithActions: Story = { + render: () => { + const actions = [ + { + id: 'view', + label: 'View', + icon: 'visibility_on', + ariaLabel: 'View item', + }, + { + id: 'edit', + label: 'Edit', + icon: 'pencil', + ariaLabel: 'Edit item', + }, + { + id: 'delete', + label: 'Delete', + icon: 'delete', + ariaLabel: 'Delete item', + }, + ]; + + return html` + + + + + + + + + + + + + + + + + + + + + + - diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx index cc104efcdf..e882435410 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx @@ -1,6 +1,12 @@ import { Component, Element, h, Host, Prop, State } from '@stencil/core'; import { Attributes, inheritAriaAttributes } from '../utils'; +interface HTMLModusWcTreeItemElement extends HTMLElement { + hasSubtree?: boolean; + expandSubTree: () => Promise; + collapseSubTree: () => Promise; +} + /** * A customizable content tree component used to display hierarchical data in a tree structure. * Uses menu items to create the tree structure with support for expanding/collapsing nodes and selection. @@ -26,6 +32,7 @@ export class ModusWcContentTree { @State() private hasSlotContent: boolean = false; @State() private searchValue: string = ''; @State() private isAddNodeMode: boolean = false; + @State() private areAllExpanded: boolean = false; componentWillLoad() { this.inheritedAttributes = inheritAriaAttributes(this.el); @@ -50,13 +57,8 @@ export class ModusWcContentTree { document.removeEventListener('click', this.handleClickOutside); } - private addMenuItem = () => { - this.searchValue = ''; - this.filterNodes(''); - }; - - private createMenuItem() { - console.log('Creating menu item:', this.searchValue); + private createTreeItem() { + console.log('Creating tree item:', this.searchValue); this.searchValue = ''; this.isAddNodeMode = false; } @@ -71,7 +73,7 @@ export class ModusWcContentTree { }; private filterNodes(searchTerm: string) { - const menuItems = this.el.querySelectorAll('modus-wc-menu-item'); + const menuItems = this.el.querySelectorAll('modus-wc-tree-item'); const normalizedSearch = searchTerm.toLowerCase().trim(); if (!normalizedSearch) { @@ -94,9 +96,9 @@ export class ModusWcContentTree { // Expand and show all parent nodes let parent = item.parentElement; while (parent && parent !== this.el) { - if (parent.tagName === 'MODUS-WC-MENU-ITEM') { + if (parent.tagName === 'MODUS-WC-TREE-ITEM') { parent.style.display = ''; - // (parent as any).expandSubmenu(); + (parent as HTMLModusWcTreeItemElement).expandSubTree(); } parent = parent.parentElement; } @@ -109,7 +111,7 @@ export class ModusWcContentTree { if (hasMatchingChildren) { (item as HTMLElement).style.display = ''; - // (item as any).expandSubmenu(); + (item as HTMLModusWcTreeItemElement).expandSubTree(); } else { (item as HTMLElement).style.display = 'none'; } @@ -121,7 +123,7 @@ export class ModusWcContentTree { element: HTMLElement, searchTerm: string ): boolean { - const childMenuItems = element.querySelectorAll('modus-wc-menu-item'); + const childMenuItems = element.querySelectorAll('modus-wc-tree-item'); for (const child of Array.from(childMenuItems)) { const label = child.getAttribute('label') || ''; @@ -138,7 +140,7 @@ export class ModusWcContentTree { if (event.key === 'Enter') { if (this.isAddNodeMode && value) { - this.createMenuItem(); + this.createTreeItem(); } return; } @@ -182,6 +184,28 @@ export class ModusWcContentTree { this.hasSlotContent = assigned.length > 0; }; + private toggleExpandCollapse = async () => { + const treeItems = this.el.querySelectorAll('modus-wc-tree-item'); + this.areAllExpanded = !this.areAllExpanded; + + const promises = Array.from(treeItems).map((item) => { + const treeItem = item as HTMLModusWcTreeItemElement; + const hasSubtree = + item.hasAttribute('has-subtree') || treeItem.hasSubtree === true; + + if (hasSubtree) { + if (this.areAllExpanded) { + return treeItem.expandSubTree(); + } else { + return treeItem.collapseSubTree(); + } + } + return Promise.resolve(); + }); + + await Promise.all(promises); + }; + render() { return ( @@ -205,19 +229,20 @@ export class ModusWcContentTree {
    - - - + shape="circle" + onClick={this.toggleExpandCollapse} + aria-label={this.areAllExpanded ? 'Collapse all' : 'Expand all'} + > + +
    @@ -236,16 +261,6 @@ export class ModusWcContentTree { weight="normal" customClass="modus-wc-content-tree-empty-text" > - - Create Node -
    )}
  • diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss new file mode 100644 index 0000000000..0732980043 --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss @@ -0,0 +1,73 @@ +/** +* Tree actions component styles +*/ + +modus-wc-tree-actions { + .modus-wc-tree-actions-container { + align-items: center; + display: flex; + + .modus-wc-tree-action-button { + .modus-wc-tree-action-icon { + color: black; + } + } + } + + .modus-wc-tree-more-actions-dropdown { + background: var(--modus-wc-color-base-page); + border: 1px solid var(--modus-wc-color-base-100); + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + display: none; + min-width: 150px; + padding: 4px 0; + position: fixed; + z-index: 1000; + + &.show { + display: block; + } + } + + .modus-wc-tree-dropdown-action { + align-items: center; + background: transparent; + border: none; + cursor: pointer; + display: flex; + gap: var(--modus-wc-spacing-sm, 0.5rem); + padding: var(--modus-wc-spacing-sm, 0.5rem) var(--modus-wc-spacing-md, 1rem); + text-align: start; + width: 100%; + + &:hover:not(.disabled) { + background-color: var(--modus-wc-color-gray-light); + } + + &.disabled { + cursor: not-allowed; + opacity: 0.5; + pointer-events: none; + } + + span { + font-size: var(--modus-wc-font-size-sm, 0.875rem); + } + } +} + +[data-theme='modus-classic-dark'], +[data-theme='modus-modern-dark'], +[data-theme='connect-dark'] { + .modus-wc-tree-more-actions-dropdown { + background: var(--modus-wc-color-trimble-gray); + border-color: var(----modus-wc-color-gray-6); + } + + .modus-wc-tree-dropdown-action { + &:hover:not(.disabled) { + background-color: var(--modus-wc-color-base-100); + } + } +} diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx new file mode 100644 index 0000000000..ebb58cb4f7 --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx @@ -0,0 +1,232 @@ +import { createPopper, Instance as PopperInstance } from '@popperjs/core'; +import { + Component, + Element, + h, + Host, + Prop, + State, + Event as StencilEvent, + EventEmitter, + Listen, +} from '@stencil/core'; + +export interface ModusTreeItemActions { + id: string; // Unique identifier for the action + icon: string; // Icon name for the action, e.g., 'edit', 'trash' + iconVariant?: 'solid' | 'outline'; // Optional variant for the icon + label: string; // Text label for the action, used for accessibility and tooltips + ariaLabel?: string; // Optional label for accessibility + disabled?: boolean; // Optional flag to disable the action +} + +@Component({ + tag: 'modus-wc-tree-actions', + styleUrl: 'modus-wc-tree-actions.scss', + shadow: false, +}) +export class ModusWcTreeActions { + private moreActionsButton!: HTMLElement; + private moreActionsDropdown!: HTMLElement; + private popperInstance: PopperInstance | null = null; + + /** Reference to the host element */ + @Element() el!: HTMLElement; + + /** List of actions to display */ + @Prop({ mutable: true }) actions?: ModusTreeItemActions[]; + + /** The size of the action buttons and icons. */ + @Prop() size: 'xs' | 'sm' | 'md' = 'xs'; + + /** Internal state for dropdown visibility */ + @State() isDropdownOpen: boolean = false; + + /** Event emitted when a dropdown is opened */ + @StencilEvent() dropdownOpened!: EventEmitter; + + /** Event emitted when an action is clicked */ + @StencilEvent() treeActionClick!: EventEmitter<{ + actionId: string; + actionName: string; + }>; + + componentDidLoad() { + document.addEventListener('click', this.handleClickOutside); + } + + componentDidUpdate() { + if (this.actions && this.actions.length > 2) { + this.initializePopper(); + } else if (this.popperInstance) { + this.popperInstance.destroy(); + this.popperInstance = null; + } + } + + disconnectedCallback() { + document.removeEventListener('click', this.handleClickOutside); + if (this.popperInstance) { + this.popperInstance.destroy(); + this.popperInstance = null; + } + } + + @Listen('dropdownOpened', { target: 'document' }) + handleOtherDropdownOpened(event: CustomEvent) { + // Close this dropdown if another one was opened + if (event.detail !== this.el && this.isDropdownOpen) { + this.isDropdownOpen = false; + } + } + + private handleActionClick = ( + action: ModusTreeItemActions, + event: MouseEvent + ) => { + event.stopPropagation(); + if (action.disabled) return; + + this.treeActionClick.emit({ + actionId: action.id, + actionName: action.label, + }); + }; + + private handleMoreActionsClick = (event: MouseEvent) => { + event.stopPropagation(); + this.isDropdownOpen = !this.isDropdownOpen; + + if (this.isDropdownOpen) { + // Emit event to close other dropdowns + this.dropdownOpened.emit(this.el); + + if (this.popperInstance) { + this.popperInstance.update(); + } + } + }; + + private handleClickOutside = (event: MouseEvent) => { + const target = event.target as HTMLElement; + + if (!this.isDropdownOpen) return; + + // Check if clicking inside this component's button or dropdown + const clickedInside = + this.moreActionsButton?.contains(target) || + this.moreActionsDropdown?.contains(target); + + // Check if clicking another more actions button + const clickedAnotherButton = target.closest('.modus-wc-tree-action-button'); + + if (!clickedInside || clickedAnotherButton) { + this.isDropdownOpen = false; + } + }; + + private initializePopper = () => { + if (this.popperInstance) { + this.popperInstance.destroy(); + this.popperInstance = null; + } + + if (!this.moreActionsButton || !this.moreActionsDropdown) { + return; + } + + this.popperInstance = createPopper( + this.moreActionsButton, + this.moreActionsDropdown, + { + placement: 'bottom', + strategy: 'absolute', + modifiers: [ + { + name: 'offset', + options: { + offset: [0, 8], + }, + }, + { + name: 'flip', + options: { + fallbackPlacements: ['top-start', 'bottom-end', 'top-end'], + }, + }, + ], + } + ); + }; + + render() { + const remainingActions = this.actions?.slice(1) || []; + + return ( + +
    + {this.actions?.slice(0, 1).map((action) => ( + this.handleActionClick(action, e)} + > + + + ))} + {remainingActions.length > 0 && ( +
    + (this.moreActionsButton = el as HTMLElement)} + onClick={this.handleMoreActionsClick} + aria-expanded={this.isDropdownOpen ? 'true' : 'false'} + aria-haspopup="true" + > + + +
    (this.moreActionsDropdown = el as HTMLElement)} + role="menu" + > + {remainingActions.map((action) => ( + + ))} +
    +
    + )} +
    +
    + ); + } +} diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/readme.md b/src/components/modus-wc-content-tree/modus-wc-tree-actions/readme.md new file mode 100644 index 0000000000..880a10de4f --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/readme.md @@ -0,0 +1,46 @@ +# modus-wc-tree-actions + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| --------- | --------- | ----------------------------------------- | ------------------------------------- | ----------- | +| `actions` | `actions` | List of actions to display | `ModusTreeItemActions[] \| undefined` | `undefined` | +| `size` | `size` | The size of the action buttons and icons. | `"md" \| "sm" \| "xs"` | `'xs'` | + + +## Events + +| Event | Description | Type | +| ----------------- | --------------------------------------- | -------------------------------------------------------- | +| `dropdownOpened` | Event emitted when a dropdown is opened | `CustomEvent` | +| `treeActionClick` | Event emitted when an action is clicked | `CustomEvent<{ actionId: string; actionName: string; }>` | + + +## Dependencies + +### Used by + + - [modus-wc-tree-item](../modus-wc-tree-item) + +### Depends on + +- [modus-wc-button](../../modus-wc-button) +- [modus-wc-icon](../../modus-wc-icon) + +### Graph +```mermaid +graph TD; + modus-wc-tree-actions --> modus-wc-button + modus-wc-tree-actions --> modus-wc-icon + modus-wc-tree-item --> modus-wc-tree-actions + style modus-wc-tree-actions fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss index 11373aa3e7..a1da473eb6 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss @@ -10,8 +10,8 @@ modus-wc-tree-item { .modus-wc-tree-item-active { background-color: var(--modus-wc-color-blue-pale); - color: var(--modus-wc-color-primary); border-radius: unset; + color: var(--modus-wc-color-primary); } .modus-wc-tree-content { @@ -20,8 +20,8 @@ modus-wc-tree-item { gap: var(--modus-wc-spacing-sm, 0.5rem); .modus-wc-tree-drag-handle { - position: absolute; left: 0; + position: absolute; } } @@ -33,6 +33,13 @@ modus-wc-tree-item { } } + .modus-wc-tree-item-actions { + align-items: center; + display: flex; + gap: var(--modus-wc-spacing-xs, 0.25rem); + margin-inline-start: auto; + } + button { align-items: center; background: transparent; @@ -54,6 +61,16 @@ modus-wc-tree-item { display: block; } } + + .modus-wc-tree-item-disabled { + cursor: not-allowed; + opacity: 0.5; + pointer-events: none; + + .modus-wc-tree-content { + cursor: not-allowed; + } + } } [data-theme='modus-classic-dark'], diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx index 59d68d6ce5..a051a3b70a 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx @@ -4,12 +4,14 @@ import { EventEmitter, h, Host, + Method, Prop, State, Event as StencilEvent, } from '@stencil/core'; import { Attributes, inheritAriaAttributes } from '../../utils'; import { convertPropsToClasses } from './modus-wc-tree-item.tailwind'; +import { ModusTreeItemActions } from '../modus-wc-tree-actions/modus-wc-tree-actions'; /** * A tree item component that represents a single node in a hierarchical tree structure. @@ -49,9 +51,15 @@ export class ModusWcTreeItem { /** Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior. */ @Prop() hasSubtree?: boolean; + /** Actions to display for this tree item. */ + @Prop() treeItemActions?: ModusTreeItemActions[]; + /** Internal state to track if subtree is expanded */ @State() isExpanded: boolean = false; + /** The size of the tree item icons and actions. */ + @Prop() size: 'xs' | 'sm' | 'md' = 'sm'; + /** Event emitted when a tree item is selected. */ @StencilEvent() itemSelect!: EventEmitter<{ value: string; @@ -61,6 +69,40 @@ export class ModusWcTreeItem { this.inheritedAttributes = inheritAriaAttributes(this.el); } + /** + * Public method to collapse the subtree if it's expanded + */ + @Method() + async collapseSubTree(): Promise { + if (this.hasSubtree && this.isExpanded) { + const submenu = this.el.querySelector( + '.modus-wc-tree-dropdown' + ) as HTMLElement; + + if (submenu) { + submenu.classList.remove('modus-wc-tree-dropdown-show'); + this.isExpanded = false; + } + } + } + + /** + * Public method to expand the subtree if it's collapsed + */ + @Method() + async expandSubTree(): Promise { + if (this.hasSubtree && !this.isExpanded) { + const submenu = this.el.querySelector( + '.modus-wc-tree-dropdown' + ) as HTMLElement; + + if (submenu) { + submenu.classList.add('modus-wc-tree-dropdown-show'); + this.isExpanded = true; + } + } + } + private getClasses(): string { const classList: string[] = ['modus-wc-tree-item']; @@ -127,6 +169,7 @@ export class ModusWcTreeItem { )} {this.hasSubtree && ( @@ -134,6 +177,7 @@ export class ModusWcTreeItem { name={this.isExpanded ? 'expand_more' : 'chevron_right'} onClick={this.handleToggleClick} customClass={`modus-wc-tree-toggle-icon ${this.isExpanded ? 'modus-wc-tree-toggle-expanded' : ''}`} + size={this.size} > )} {this.checkbox && ( @@ -141,12 +185,19 @@ export class ModusWcTreeItem { aria-label="Checkbox" disabled={this.disabled} value={!!this.selected} + size="sm" /> )}
    {this.label}
    +
    + +
    diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md b/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md index cc24773c0a..277d7e3f3c 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md @@ -11,16 +11,18 @@ A tree item component that represents a single node in a hierarchical tree struc ## Properties -| Property | Attribute | Description | Type | Default | -| -------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------- | ----------- | -| `checkbox` | `checkbox` | If true, renders a checkbox at the start of the tree item. | `boolean \| undefined` | `false` | -| `customClass` | `custom-class` | Custom CSS class to apply to the li element. | `string \| undefined` | `''` | -| `disabled` | `disabled` | The disabled state of the tree item. | `boolean \| undefined` | `undefined` | -| `dragHandle` | `drag-handle` | If true, renders a drag handle icon at the start of the tree item. | `boolean \| undefined` | `false` | -| `hasSubtree` | `has-subtree` | Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior. | `boolean \| undefined` | `undefined` | -| `label` _(required)_ | `label` | The text label displayed for the tree item. | `string` | `undefined` | -| `selected` | `selected` | The selected state of the tree item. | `boolean \| undefined` | `undefined` | -| `value` | `value` | The unique identifying value of the tree item. | `string` | `''` | +| Property | Attribute | Description | Type | Default | +| -------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ----------- | +| `checkbox` | `checkbox` | If true, renders a checkbox at the start of the tree item. | `boolean \| undefined` | `false` | +| `customClass` | `custom-class` | Custom CSS class to apply to the li element. | `string \| undefined` | `''` | +| `disabled` | `disabled` | The disabled state of the tree item. | `boolean \| undefined` | `undefined` | +| `dragHandle` | `drag-handle` | If true, renders a drag handle icon at the start of the tree item. | `boolean \| undefined` | `false` | +| `hasSubtree` | `has-subtree` | Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior. | `boolean \| undefined` | `undefined` | +| `label` _(required)_ | `label` | The text label displayed for the tree item. | `string` | `undefined` | +| `selected` | `selected` | The selected state of the tree item. | `boolean \| undefined` | `undefined` | +| `size` | `size` | The size of the tree item icons and actions. | `"md" \| "sm" \| "xs"` | `'sm'` | +| `treeItemActions` | `tree-item-actions` | Actions to display for this tree item. | `ModusTreeItemActions[] \| undefined` | `undefined` | +| `value` | `value` | The unique identifying value of the tree item. | `string` | `''` | ## Events @@ -30,19 +32,46 @@ A tree item component that represents a single node in a hierarchical tree struc | `itemSelect` | Event emitted when a tree item is selected. | `CustomEvent<{ value: string; }>` | +## Methods + +### `collapseSubTree() => Promise` + +Public method to collapse the subtree if it's expanded + +#### Returns + +Type: `Promise` + + + +### `expandSubTree() => Promise` + +Public method to expand the subtree if it's collapsed + +#### Returns + +Type: `Promise` + + + + ## Dependencies ### Depends on - [modus-wc-icon](../../modus-wc-icon) - [modus-wc-checkbox](../../modus-wc-checkbox) +- [modus-wc-tree-actions](../modus-wc-tree-actions) ### Graph ```mermaid graph TD; modus-wc-tree-item --> modus-wc-icon modus-wc-tree-item --> modus-wc-checkbox + modus-wc-tree-item --> modus-wc-tree-actions modus-wc-checkbox --> modus-wc-input-label + modus-wc-tree-actions --> modus-wc-button + modus-wc-tree-actions --> modus-wc-icon style modus-wc-tree-item fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/src/components/modus-wc-content-tree/readme.md b/src/components/modus-wc-content-tree/readme.md index cfdd15e3c7..5a23915a08 100644 --- a/src/components/modus-wc-content-tree/readme.md +++ b/src/components/modus-wc-content-tree/readme.md @@ -23,17 +23,17 @@ Uses menu items to create the tree structure with support for expanding/collapsi ### Depends on - [modus-wc-text-input](../modus-wc-text-input) +- [modus-wc-button](../modus-wc-button) - [modus-wc-icon](../modus-wc-icon) - [modus-wc-typography](../modus-wc-typography) -- [modus-wc-button](../modus-wc-button) ### Graph ```mermaid graph TD; modus-wc-content-tree --> modus-wc-text-input + modus-wc-content-tree --> modus-wc-button modus-wc-content-tree --> modus-wc-icon modus-wc-content-tree --> modus-wc-typography - modus-wc-content-tree --> modus-wc-button modus-wc-text-input --> modus-wc-input-label modus-wc-text-input --> modus-wc-input-feedback modus-wc-input-feedback --> modus-wc-icon diff --git a/src/custom-elements.json b/src/custom-elements.json index 06f8a27a7b..a1f7b489f8 100644 --- a/src/custom-elements.json +++ b/src/custom-elements.json @@ -2353,6 +2353,109 @@ } ] }, + { + "kind": "javascript-module", + "path": "src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx", + "declarations": [ + { + "kind": "class", + "description": "", + "name": "ModusWcTreeActions", + "members": [ + { + "kind": "field", + "name": "el", + "type": { + "text": "HTMLElement" + }, + "description": "Reference to the host element" + }, + { + "kind": "method", + "name": "handleOtherDropdownOpened", + "parameters": [ + { + "name": "event", + "type": { + "text": "CustomEvent" + } + } + ] + }, + { + "kind": "field", + "name": "isDropdownOpen", + "type": { + "text": "boolean" + }, + "default": "false", + "description": "Internal state for dropdown visibility" + }, + { + "kind": "method", + "name": "render" + } + ], + "attributes": [ + { + "name": "actions", + "fieldName": "actions", + "description": "List of actions to display", + "type": { + "text": "ModusTreeItemActions[]" + } + }, + { + "name": "size", + "fieldName": "size", + "default": "'xs'", + "description": "The size of the action buttons and icons.", + "type": { + "text": "'xs' | 'sm' | 'md'" + } + } + ], + "tagName": "modus-wc-tree-actions", + "events": [ + { + "kind": "field", + "name": "dropdownOpened", + "type": { + "text": "EventEmitter" + }, + "description": "Event emitted when a dropdown is opened" + }, + { + "kind": "field", + "name": "treeActionClick", + "type": { + "text": "EventEmitter<{\r\n actionId: string;\r\n actionName: string;\r\n }>" + }, + "description": "Event emitted when an action is clicked" + } + ], + "customElement": true + } + ], + "exports": [ + { + "kind": "js", + "name": "ModusWcTreeActions", + "declaration": { + "name": "ModusWcTreeActions", + "module": "src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx" + } + }, + { + "kind": "custom-element-definition", + "name": "modus-wc-tree-actions", + "declaration": { + "name": "ModusWcTreeActions", + "module": "src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx" + } + } + ] + }, { "kind": "javascript-module", "path": "src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx", @@ -2362,6 +2465,16 @@ "description": "A tree item component that represents a single node in a hierarchical tree structure.", "name": "ModusWcTreeItem", "members": [ + { + "kind": "method", + "name": "collapseSubTree", + "return": { + "type": { + "text": "Promise" + } + }, + "description": "Public method to collapse the subtree if it's expanded" + }, { "kind": "field", "name": "el", @@ -2370,6 +2483,16 @@ }, "description": "Reference to the host element" }, + { + "kind": "method", + "name": "expandSubTree", + "return": { + "type": { + "text": "Promise" + } + }, + "description": "Public method to expand the subtree if it's collapsed" + }, { "kind": "field", "name": "isExpanded", @@ -2444,6 +2567,23 @@ "text": "boolean" } }, + { + "name": "size", + "fieldName": "size", + "default": "'sm'", + "description": "The size of the tree item icons and actions.", + "type": { + "text": "'xs' | 'sm' | 'md'" + } + }, + { + "name": "tree-item-actions", + "fieldName": "treeItemActions", + "description": "Actions to display for this tree item.", + "type": { + "text": "ModusTreeItemActions[]" + } + }, { "name": "value", "fieldName": "value", @@ -2460,7 +2600,7 @@ "kind": "field", "name": "itemSelect", "type": { - "text": "EventEmitter<{\r\n value: string;\r\n selected?: boolean;\r\n }>" + "text": "EventEmitter<{\r\n value: string;\r\n }>" }, "description": "Event emitted when a tree item is selected." } @@ -2511,7 +2651,7 @@ { "name": "event", "type": { - "text": "CustomEvent<{ value: string; selected?: boolean }>" + "text": "CustomEvent<{ value: string }>" } } ] From bf962bb3e7d242a99fabf6d463ee17c6ad9be1f9 Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Wed, 18 Feb 2026 18:35:04 +0530 Subject: [PATCH 11/39] 662 - Add multi-select feature --- src/components.d.ts | 16 +-- .../modus-wc-content-tree.stories.ts | 14 +- .../modus-wc-tree-actions.scss | 2 +- .../modus-wc-tree-actions.tsx | 3 +- .../modus-wc-tree-actions/readme.md | 2 +- .../modus-wc-tree-item.scss | 1 + .../modus-wc-tree-item/modus-wc-tree-item.tsx | 120 ++++++++++++++++-- .../modus-wc-tree-item/readme.md | 3 +- .../modus-wc-tree-view/modus-wc-tree-view.tsx | 12 +- src/custom-elements.json | 21 ++- 10 files changed, 127 insertions(+), 67 deletions(-) diff --git a/src/components.d.ts b/src/components.d.ts index 52d325feb6..f4e9ed36ef 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -2021,7 +2021,7 @@ export namespace Components { /** * The size of the action buttons and icons. */ - "size": 'xs' | 'sm' | 'md'; + "size": ModusSize; } /** * A tree item component that represents a single node in a hierarchical tree structure. @@ -2043,10 +2043,6 @@ export namespace Components { * The disabled state of the tree item. */ "disabled"?: boolean; - /** - * If true, renders a drag handle icon at the start of the tree item. - */ - "dragHandle"?: boolean; /** * Public method to expand the subtree if it's collapsed */ @@ -2066,7 +2062,7 @@ export namespace Components { /** * The size of the tree item icons and actions. */ - "size": 'xs' | 'sm' | 'md'; + "size": ModusSize; /** * Actions to display for this tree item. */ @@ -5587,7 +5583,7 @@ declare namespace LocalJSX { /** * The size of the action buttons and icons. */ - "size"?: 'xs' | 'sm' | 'md'; + "size"?: ModusSize; } /** * A tree item component that represents a single node in a hierarchical tree structure. @@ -5605,10 +5601,6 @@ declare namespace LocalJSX { * The disabled state of the tree item. */ "disabled"?: boolean; - /** - * If true, renders a drag handle icon at the start of the tree item. - */ - "dragHandle"?: boolean; /** * Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior. */ @@ -5630,7 +5622,7 @@ declare namespace LocalJSX { /** * The size of the tree item icons and actions. */ - "size"?: 'xs' | 'sm' | 'md'; + "size"?: ModusSize; /** * Actions to display for this tree item. */ diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts index 245c4ee847..cd3911755c 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts @@ -1,7 +1,7 @@ import { withActions } from '@storybook/addon-actions/decorator'; import { Meta, StoryObj } from '@storybook/web-components'; import { html } from 'lit'; -import { ifDefined } from 'lit/directives/if-defined.js'; +// import { ifDefined } from 'lit/directives/if-defined.js'; interface ContentTreeArgs { 'custom-class'?: string; @@ -32,18 +32,6 @@ export default meta; type Story = StoryObj; export const Default: Story = { - render: (args) => { - return html` - - - `; - }, -}; - -export const UsingContentTreeItem: Story = { render: () => { const actions = [ { diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss index 0732980043..c0aebb090e 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss @@ -62,7 +62,7 @@ modus-wc-tree-actions { [data-theme='connect-dark'] { .modus-wc-tree-more-actions-dropdown { background: var(--modus-wc-color-trimble-gray); - border-color: var(----modus-wc-color-gray-6); + border-color: var(--modus-wc-color-gray-6); } .modus-wc-tree-dropdown-action { diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx index ebb58cb4f7..c3431a9c17 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx @@ -10,6 +10,7 @@ import { EventEmitter, Listen, } from '@stencil/core'; +import { ModusSize } from '../../types'; export interface ModusTreeItemActions { id: string; // Unique identifier for the action @@ -37,7 +38,7 @@ export class ModusWcTreeActions { @Prop({ mutable: true }) actions?: ModusTreeItemActions[]; /** The size of the action buttons and icons. */ - @Prop() size: 'xs' | 'sm' | 'md' = 'xs'; + @Prop() size: ModusSize = 'md'; /** Internal state for dropdown visibility */ @State() isDropdownOpen: boolean = false; diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/readme.md b/src/components/modus-wc-content-tree/modus-wc-tree-actions/readme.md index 880a10de4f..2a4795ed57 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/readme.md +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/readme.md @@ -10,7 +10,7 @@ | Property | Attribute | Description | Type | Default | | --------- | --------- | ----------------------------------------- | ------------------------------------- | ----------- | | `actions` | `actions` | List of actions to display | `ModusTreeItemActions[] \| undefined` | `undefined` | -| `size` | `size` | The size of the action buttons and icons. | `"md" \| "sm" \| "xs"` | `'xs'` | +| `size` | `size` | The size of the action buttons and icons. | `"lg" \| "md" \| "sm"` | `'md'` | ## Events diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss index a1da473eb6..e8ad079ebf 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss @@ -59,6 +59,7 @@ modus-wc-tree-item { &.modus-wc-tree-dropdown-show { display: block; + margin-inline-start: 1.5rem; } } diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx index a051a3b70a..686285133a 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx @@ -12,6 +12,20 @@ import { import { Attributes, inheritAriaAttributes } from '../../utils'; import { convertPropsToClasses } from './modus-wc-tree-item.tailwind'; import { ModusTreeItemActions } from '../modus-wc-tree-actions/modus-wc-tree-actions'; +import { ModusSize } from '../../types'; + +interface IMenuItemElement extends HTMLElement { + /** The unique identifying value of the tree item. */ + value: string; + /** The selected state of the tree item. */ + selected?: boolean; + /** Whether the item has a checkbox (used for selection in tree structure) */ + checkbox?: boolean; + /** Whether this item has a submenu (used for tree structure) */ + hasSubmenu?: boolean; + /** Whether the checkbox is in an indeterminate state (only applicable if checkbox is true) */ + isIndeterminate?: boolean; +} /** * A tree item component that represents a single node in a hierarchical tree structure. @@ -33,9 +47,6 @@ export class ModusWcTreeItem { /** If true, renders a checkbox at the start of the tree item. */ @Prop() checkbox?: boolean = false; - /** If true, renders a drag handle icon at the start of the tree item. */ - @Prop() dragHandle?: boolean = false; - /** The text label displayed for the tree item. */ @Prop() label!: string; @@ -54,14 +65,17 @@ export class ModusWcTreeItem { /** Actions to display for this tree item. */ @Prop() treeItemActions?: ModusTreeItemActions[]; + /** The size of the tree item icons and actions. */ + @Prop() size: ModusSize = 'md'; + /** Internal state to track if subtree is expanded */ @State() isExpanded: boolean = false; - /** The size of the tree item icons and actions. */ - @Prop() size: 'xs' | 'sm' | 'md' = 'sm'; + /** Internal state to track if checkbox is in indeterminate state */ + @State() isIndeterminate: boolean = false; /** Event emitted when a tree item is selected. */ - @StencilEvent() itemSelect!: EventEmitter<{ + @StencilEvent({ bubbles: true, composed: true }) itemSelect!: EventEmitter<{ value: string; }>; @@ -69,6 +83,16 @@ export class ModusWcTreeItem { this.inheritedAttributes = inheritAriaAttributes(this.el); } + componentDidLoad() { + if (this.hasSubtree) { + this.el.addEventListener('itemSelect', this.updateIndeterminateState); + } + } + + disconnectedCallback() { + this.el.removeEventListener('itemSelect', this.updateIndeterminateState); + } + /** * Public method to collapse the subtree if it's expanded */ @@ -142,10 +166,77 @@ export class ModusWcTreeItem { this.handleEmittedSelect(); }; + private handleCheckboxKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + this.handleCheckboxClick(); + } + }; + + private updateIndeterminateState = (e: Event) => { + if (e.target === this.el) return; + + if (!this.hasSubtree || !this.checkbox) return; + + const submenu = this.el.querySelector('.modus-wc-tree-dropdown'); + if (!submenu) return; + + const childMenuItems = Array.from(submenu.children).filter( + (el) => + el.tagName === 'MODUS-WC-TREE-ITEM' && (el as IMenuItemElement).checkbox + ) as IMenuItemElement[]; + + let selectedCount = 0; + + childMenuItems.forEach((item) => { + if (item.selected) selectedCount++; + }); + + this.isIndeterminate = + selectedCount > 0 && selectedCount < childMenuItems.length; + + this.selected = selectedCount === childMenuItems.length; + }; + + private updateChildrenSelection = (selected: boolean) => { + if (!this.hasSubtree) return; + + const submenu = this.el.querySelector('.modus-wc-tree-dropdown'); + if (!submenu) return; + + const descendants = Array.from( + submenu.querySelectorAll('modus-wc-tree-item') + ) as IMenuItemElement[]; + + descendants.forEach((item) => { + if (!item.checkbox) return; + + item.selected = selected; + item.isIndeterminate = false; + + const checkbox = item.querySelector('modus-wc-checkbox'); + if (checkbox) { + checkbox.setAttribute('value', selected.toString()); + checkbox.removeAttribute('indeterminate'); + } + }); + }; + private handleEmittedSelect = () => { this.itemSelect.emit({ value: this.value }); }; + private handleCheckboxClick = () => { + const newValue = !this.selected || this.isIndeterminate; + + this.selected = newValue; + this.isIndeterminate = false; + this.updateChildrenSelection(newValue); + + this.itemSelect.emit({ value: this.value }); + }; + render() { return ( @@ -165,13 +256,6 @@ export class ModusWcTreeItem { this.selected ? 'modus-wc-tree-item-active' : '' }`} > - {this.dragHandle && ( - - )} {this.hasSubtree && ( { + e.stopPropagation(); + this.handleCheckboxClick(); + }} + onKeyDown={(e) => { + this.handleCheckboxKeyDown(e); + }} /> )} diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md b/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md index 277d7e3f3c..de6ee785e7 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md @@ -16,11 +16,10 @@ A tree item component that represents a single node in a hierarchical tree struc | `checkbox` | `checkbox` | If true, renders a checkbox at the start of the tree item. | `boolean \| undefined` | `false` | | `customClass` | `custom-class` | Custom CSS class to apply to the li element. | `string \| undefined` | `''` | | `disabled` | `disabled` | The disabled state of the tree item. | `boolean \| undefined` | `undefined` | -| `dragHandle` | `drag-handle` | If true, renders a drag handle icon at the start of the tree item. | `boolean \| undefined` | `false` | | `hasSubtree` | `has-subtree` | Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior. | `boolean \| undefined` | `undefined` | | `label` _(required)_ | `label` | The text label displayed for the tree item. | `string` | `undefined` | | `selected` | `selected` | The selected state of the tree item. | `boolean \| undefined` | `undefined` | -| `size` | `size` | The size of the tree item icons and actions. | `"md" \| "sm" \| "xs"` | `'sm'` | +| `size` | `size` | The size of the tree item icons and actions. | `"lg" \| "md" \| "sm"` | `'md'` | | `treeItemActions` | `tree-item-actions` | Actions to display for this tree item. | `ModusTreeItemActions[] \| undefined` | `undefined` | | `value` | `value` | The unique identifying value of the tree item. | `string` | `''` | diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx index 67bfdcdb2f..2b5895d098 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx @@ -1,4 +1,4 @@ -import { Component, Element, h, Host, Listen, Prop } from '@stencil/core'; +import { Component, Element, h, Host, Prop } from '@stencil/core'; import { Attributes, inheritAriaAttributes } from '../../utils'; /** @@ -26,16 +26,6 @@ export class ModusWcTreeView { this.inheritedAttributes = inheritAriaAttributes(this.el); } - @Listen('itemSelect') - handleItemSelect(event: CustomEvent<{ value: string }>) { - const target = event.target as HTMLElement; - - const allItems = this.el.querySelectorAll('modus-wc-tree-item'); - allItems.forEach((item) => { - (item as HTMLModusWcTreeItemElement).selected = item === target; - }); - } - private getClasses(): string { if (this.isSubList) { const classList: string[] = ['modus-wc-tree-dropdown']; diff --git a/src/custom-elements.json b/src/custom-elements.json index a1f7b489f8..3c9b73ef51 100644 --- a/src/custom-elements.json +++ b/src/custom-elements.json @@ -2502,6 +2502,15 @@ "default": "false", "description": "Internal state to track if subtree is expanded" }, + { + "kind": "field", + "name": "isIndeterminate", + "type": { + "text": "boolean" + }, + "default": "false", + "description": "Internal state to track if checkbox is in indeterminate state" + }, { "kind": "method", "name": "render" @@ -2644,18 +2653,6 @@ }, "description": "Reference to the host element" }, - { - "kind": "method", - "name": "handleItemSelect", - "parameters": [ - { - "name": "event", - "type": { - "text": "CustomEvent<{ value: string }>" - } - } - ] - }, { "kind": "method", "name": "render" From b5e66b6a822004602b153e999ac7760b005f02d0 Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:55:14 +0530 Subject: [PATCH 12/39] 662 - ui fixes --- .../modus-wc-content-tree.scss | 19 +- .../modus-wc-content-tree.stories.ts | 221 ++++++++++-------- .../modus-wc-content-tree.tsx | 116 ++++----- .../modus-wc-tree-actions.scss | 21 +- .../modus-wc-tree-actions.tsx | 1 - .../modus-wc-tree-item.scss | 20 ++ .../modus-wc-tree-item/modus-wc-tree-item.tsx | 27 +-- .../modus-wc-tree-item/readme.md | 2 +- .../modus-wc-tree-view/modus-wc-tree-view.tsx | 16 +- .../modus-wc-content-tree/readme.md | 10 +- 10 files changed, 262 insertions(+), 191 deletions(-) diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index 9dc5d4edfe..75ce8b2dd0 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -5,7 +5,7 @@ modus-wc-content-tree { background-color: var(--modus-wc-color-base-page); - border: 1px solid var(--modus-wc-color-border-default, #d1d5db); + border: 1px solid var(--modus-wc-color-base-100); display: block; min-width: 320px; width: 100%; @@ -18,8 +18,9 @@ modus-wc-content-tree { margin-top: var(--modus-wc-spacing-md, 1rem); padding-bottom: var(--modus-wc-spacing-sm, 0.75rem); - .modus-wc-icon { + .modus-wc-content-tree-action-icon { cursor: pointer; + color: var(--modus-wc-color-black); } } @@ -33,6 +34,10 @@ modus-wc-content-tree { min-height: 500px; } + li.modus-wc-tree-item-selected:not(.modus-wc-tree-dropdown-show li) { + border-inline-start: 2px solid var(--modus-wc-color-primary); + } + .modus-wc-content-tree-header { padding: var(--modus-wc-spacing-md, 1rem); } @@ -57,3 +62,13 @@ modus-wc-content-tree { } } } + +[data-theme='modus-classic-dark'], +[data-theme='modus-modern-dark'], +[data-theme='connect-dark'] modus-wc-content-tree { + .modus-wc-content-tree-actions { + .modus-wc-content-tree-action-icon { + color: var(--modus-wc-color-gray-light); + } + } +} diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts index cd3911755c..f1c9cf992f 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts @@ -6,6 +6,8 @@ import { html } from 'lit'; interface ContentTreeArgs { 'custom-class'?: string; 'search-placeholder'?: string; + 'include-search'?: boolean; + 'include-actions'?: boolean; } const meta: Meta = { @@ -13,11 +15,19 @@ const meta: Meta = { component: 'modus-wc-content-tree', args: { 'search-placeholder': 'Search...', + 'include-search': true, + 'include-actions': true, }, argTypes: { 'search-placeholder': { control: { type: 'text' }, }, + 'include-search': { + control: { type: 'boolean' }, + }, + 'include-actions': { + control: { type: 'boolean' }, + }, }, decorators: [withActions], parameters: { @@ -32,79 +42,78 @@ export default meta; type Story = StoryObj; export const Default: Story = { - render: () => { - const actions = [ - { - id: 'view', - label: 'View', - icon: 'visibility_on', - ariaLabel: 'View item', - }, - { - id: 'edit', - label: 'Edit', - icon: 'pencil', - ariaLabel: 'Edit item', - }, - { - id: 'delete', - label: 'Delete', - icon: 'delete', - ariaLabel: 'Delete item', - }, - ]; - + render: (args) => { return html` - + - - - - - + + + + + + + + + + + + - + + + + + + @@ -113,63 +122,82 @@ export const Default: Story = { label="Projects" .hasSubtree=${true} value="projects" - .treeItemActions=${actions} > - - - - - + + + + + + + + - + + + + + + + + + + + + `; }, }; - export const WithActions: Story = { - render: () => { + render: (args) => { const actions = [ { id: 'view', @@ -192,7 +220,12 @@ export const WithActions: Story = { ]; return html` - + { const target = event.target as HTMLInputElement; this.searchValue = target.value; - - if (!this.isAddNodeMode) { - this.filterNodes(this.searchValue); - } + this.filterNodes(this.searchValue); }; private filterNodes(searchTerm: string) { @@ -136,40 +135,12 @@ export class ModusWcContentTree { } private handleInputKeyDown = (event: KeyboardEvent) => { - const value = this.searchValue.trim(); - - if (event.key === 'Enter') { - if (this.isAddNodeMode && value) { - this.createTreeItem(); - } - return; - } - if (event.key === 'Escape') { - this.isAddNodeMode = false; this.searchValue = ''; this.filterNodes(''); } }; - private handleClickOutside = (event: MouseEvent) => { - const target = event.target as HTMLElement; - const searchSection = this.el.querySelector( - '.modus-wc-content-tree-search' - ); - const emptySection = this.el.querySelector('.modus-wc-content-tree-empty'); - - if ( - this.isAddNodeMode && - searchSection && - !searchSection.contains(target) && - (!emptySection || !emptySection.contains(target)) - ) { - this.isAddNodeMode = false; - this.searchValue = ''; - } - }; - private updateSlotContent = () => { if (!this.slotEl) return; @@ -214,36 +185,41 @@ export class ModusWcContentTree { {...this.inheritedAttributes} >
    - - -
    - - + +
    + )} + + {this.includeActions && ( +
    + - -
    + shape="circle" + onClick={this.toggleExpandCollapse} + aria-label={ + this.areAllExpanded ? 'Collapse all' : 'Expand all' + } + > + + +
    + )}
    diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss index c0aebb090e..5bba6502ad 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss @@ -8,8 +8,15 @@ modus-wc-tree-actions { display: flex; .modus-wc-tree-action-button { - .modus-wc-tree-action-icon { - color: black; + background: transparent; + color: var(--modus-wc-color-black); + border: none; + cursor: pointer; + visibility: hidden; + + &:hover { + background: transparent; + color: var(--modus-wc-color-black); } } } @@ -60,6 +67,16 @@ modus-wc-tree-actions { [data-theme='modus-classic-dark'], [data-theme='modus-modern-dark'], [data-theme='connect-dark'] { + .modus-wc-tree-actions-container { + .modus-wc-tree-action-button { + color: var(--modus-wc-color-gray-light); + + &:hover { + color: var(--modus-wc-color-white); + } + } + } + .modus-wc-tree-more-actions-dropdown { background: var(--modus-wc-color-trimble-gray); border-color: var(--modus-wc-color-gray-6); diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx index c3431a9c17..9eb865f3f3 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx @@ -198,7 +198,6 @@ export class ModusWcTreeActions {
    -
    - {this.hasSubtree && ( - - )} +
    + + {this.checkbox && ( { e.stopPropagation(); diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md b/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md index de6ee785e7..a6bfe838d5 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md @@ -19,7 +19,7 @@ A tree item component that represents a single node in a hierarchical tree struc | `hasSubtree` | `has-subtree` | Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior. | `boolean \| undefined` | `undefined` | | `label` _(required)_ | `label` | The text label displayed for the tree item. | `string` | `undefined` | | `selected` | `selected` | The selected state of the tree item. | `boolean \| undefined` | `undefined` | -| `size` | `size` | The size of the tree item icons and actions. | `"lg" \| "md" \| "sm"` | `'md'` | +| `size` | `size` | The size of the tree item icons and actions. | `"lg" \| "md" \| "sm"` | `'sm'` | | `treeItemActions` | `tree-item-actions` | Actions to display for this tree item. | `ModusTreeItemActions[] \| undefined` | `undefined` | | `value` | `value` | The unique identifying value of the tree item. | `string` | `''` | diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx index 2b5895d098..14b92cc08d 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx @@ -1,4 +1,4 @@ -import { Component, Element, h, Host, Prop } from '@stencil/core'; +import { Component, Element, h, Host, Listen, Prop } from '@stencil/core'; import { Attributes, inheritAriaAttributes } from '../../utils'; /** @@ -26,6 +26,20 @@ export class ModusWcTreeView { this.inheritedAttributes = inheritAriaAttributes(this.el); } + @Listen('itemSelect') + handleItemSelect(event: CustomEvent<{ value: string }>) { + const target = event.target as HTMLElement; + if (!target) return; + + const allContents = this.el.querySelectorAll('.modus-wc-tree-content'); + allContents.forEach((content) => + content.classList.remove('modus-wc-tree-item-active') + ); + + const targetContent = target.querySelector('.modus-wc-tree-content'); + targetContent?.classList.add('modus-wc-tree-item-active'); + } + private getClasses(): string { if (this.isSubList) { const classList: string[] = ['modus-wc-tree-dropdown']; diff --git a/src/components/modus-wc-content-tree/readme.md b/src/components/modus-wc-content-tree/readme.md index 5a23915a08..4f60bb3fd9 100644 --- a/src/components/modus-wc-content-tree/readme.md +++ b/src/components/modus-wc-content-tree/readme.md @@ -12,10 +12,12 @@ Uses menu items to create the tree structure with support for expanding/collapsi ## Properties -| Property | Attribute | Description | Type | Default | -| ------------------- | -------------------- | ------------------------------------------- | --------------------- | ------------- | -| `customClass` | `custom-class` | Custom CSS class to apply to the component. | `string \| undefined` | `''` | -| `searchPlaceholder` | `search-placeholder` | Placeholder text for the search input. | `string \| undefined` | `'Search...'` | +| Property | Attribute | Description | Type | Default | +| ------------------- | -------------------- | ----------------------------------------------------------------- | ---------------------- | ------------- | +| `customClass` | `custom-class` | Custom CSS class to apply to the component. | `string \| undefined` | `''` | +| `includeActions` | `include-actions` | If true, displays the action buttons (expand/collapse all, etc.). | `boolean \| undefined` | `true` | +| `includeSearch` | `include-search` | If true, displays the search input to filter tree items. | `boolean \| undefined` | `true` | +| `searchPlaceholder` | `search-placeholder` | Placeholder text for the search input. | `string \| undefined` | `'Search...'` | ## Dependencies From 2cb744ed43fd4f16bad3c13695b1b1827c7e5b60 Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:08:32 +0530 Subject: [PATCH 13/39] 662 - add multi select event and ui fixes --- src/components.d.ts | 27 +++++++ .../modus-wc-content-tree.stories.ts | 79 ++++++++++++++++++- .../modus-wc-tree-actions.scss | 5 +- .../modus-wc-tree-actions.tsx | 18 ++--- .../modus-wc-tree-item/modus-wc-tree-item.tsx | 25 +++++- .../modus-wc-tree-item/readme.md | 7 +- src/custom-elements.json | 55 ++++++++++--- 7 files changed, 182 insertions(+), 34 deletions(-) diff --git a/src/components.d.ts b/src/components.d.ts index f4e9ed36ef..e8d31be81b 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -526,6 +526,14 @@ export namespace Components { * Custom CSS class to apply to the component. */ "customClass"?: string; + /** + * If true, displays the action buttons (expand/collapse all, etc.). + */ + "includeActions"?: boolean; + /** + * If true, displays the search input to filter tree items. + */ + "includeSearch"?: boolean; /** * Placeholder text for the search input. */ @@ -3162,6 +3170,10 @@ declare global { interface HTMLModusWcTreeItemElementEventMap { "itemSelect": { value: string; + selected?: boolean; + }; + "selectionsChange": { + selectedValues: string[]; }; } /** @@ -3821,6 +3833,14 @@ declare namespace LocalJSX { * Custom CSS class to apply to the component. */ "customClass"?: string; + /** + * If true, displays the action buttons (expand/collapse all, etc.). + */ + "includeActions"?: boolean; + /** + * If true, displays the search input to filter tree items. + */ + "includeSearch"?: boolean; /** * Placeholder text for the search input. */ @@ -5614,6 +5634,13 @@ declare namespace LocalJSX { */ "onItemSelect"?: (event: ModusWcTreeItemCustomEvent<{ value: string; + selected?: boolean; + }>) => void; + /** + * Event emitted when checkbox selection changes in multi-select mode. + */ + "onSelectionsChange"?: (event: ModusWcTreeItemCustomEvent<{ + selectedValues: string[]; }>) => void; /** * The selected state of the tree item. diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts index f1c9cf992f..a21b1f2599 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts @@ -32,7 +32,7 @@ const meta: Meta = { decorators: [withActions], parameters: { actions: { - handles: ['itemSelect', 'treeActionClick'], + handles: ['itemSelect', 'treeActionClick', 'selectionsChange'], }, }, }; @@ -42,6 +42,56 @@ export default meta; type Story = StoryObj; export const Default: Story = { + render: (args) => { + return html` + + + + + + + + + + + + + + + + + + + + + + + + + + + `; + }, +}; + +export const MultiSelect: Story = { render: (args) => { return html` { @@ -109,19 +111,11 @@ export class ModusWcTreeActions { }; private handleClickOutside = (event: MouseEvent) => { - const target = event.target as HTMLElement; - if (!this.isDropdownOpen) return; - // Check if clicking inside this component's button or dropdown - const clickedInside = - this.moreActionsButton?.contains(target) || - this.moreActionsDropdown?.contains(target); - - // Check if clicking another more actions button - const clickedAnotherButton = target.closest('.modus-wc-tree-action-button'); - - if (!clickedInside || clickedAnotherButton) { + const target = event.target as HTMLElement; + const clickedButton = this.moreActionsButton?.contains(target); + if (!clickedButton) { this.isDropdownOpen = false; } }; diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx index d454c1ddec..aa27d720b1 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx @@ -77,6 +77,13 @@ export class ModusWcTreeItem { /** Event emitted when a tree item is selected. */ @StencilEvent({ bubbles: true, composed: true }) itemSelect!: EventEmitter<{ value: string; + selected?: boolean; + }>; + + /** Event emitted when checkbox selection changes in multi-select mode. */ + @StencilEvent({ bubbles: true, composed: true }) + selectionsChange!: EventEmitter<{ + selectedValues: string[]; }>; componentWillLoad() { @@ -224,7 +231,8 @@ export class ModusWcTreeItem { }; private handleEmittedSelect = () => { - this.itemSelect.emit({ value: this.value }); + if (this.checkbox) return; + this.itemSelect.emit({ value: this.value, selected: this.selected }); }; private handleCheckboxClick = () => { @@ -234,7 +242,20 @@ export class ModusWcTreeItem { this.isIndeterminate = false; this.updateChildrenSelection(newValue); - this.itemSelect.emit({ value: this.value }); + // Emit selectionChange event with all selected values for multi-select mode + const rootTreeView = this.el + .closest('modus-wc-content-tree') + ?.querySelector('modus-wc-tree-view'); + if (rootTreeView) { + const allTreeItems = Array.from( + rootTreeView.querySelectorAll('modus-wc-tree-item') + ) as IMenuItemElement[]; + const selectedValues = allTreeItems + .filter((item) => item.checkbox && item.selected) + .map((item) => item.value); + + this.selectionsChange.emit({ selectedValues }); + } }; render() { diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md b/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md index a6bfe838d5..0b66b5a693 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md @@ -26,9 +26,10 @@ A tree item component that represents a single node in a hierarchical tree struc ## Events -| Event | Description | Type | -| ------------ | ------------------------------------------- | --------------------------------- | -| `itemSelect` | Event emitted when a tree item is selected. | `CustomEvent<{ value: string; }>` | +| Event | Description | Type | +| ------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------ | +| `itemSelect` | Event emitted when a tree item is selected. | `CustomEvent<{ value: string; selected?: boolean \| undefined; }>` | +| `selectionsChange` | Event emitted when checkbox selection changes in multi-select mode. | `CustomEvent<{ selectedValues: string[]; }>` | ## Methods diff --git a/src/custom-elements.json b/src/custom-elements.json index 3c9b73ef51..6bacb0eb41 100644 --- a/src/custom-elements.json +++ b/src/custom-elements.json @@ -2319,6 +2319,24 @@ "text": "string" } }, + { + "name": "include-actions", + "fieldName": "includeActions", + "default": "true", + "description": "If true, displays the action buttons (expand/collapse all, etc.).", + "type": { + "text": "boolean" + } + }, + { + "name": "include-search", + "fieldName": "includeSearch", + "default": "true", + "description": "If true, displays the search input to filter tree items.", + "type": { + "text": "boolean" + } + }, { "name": "search-placeholder", "fieldName": "searchPlaceholder", @@ -2408,10 +2426,10 @@ { "name": "size", "fieldName": "size", - "default": "'xs'", + "default": "'md'", "description": "The size of the action buttons and icons.", "type": { - "text": "'xs' | 'sm' | 'md'" + "text": "ModusSize" } } ], @@ -2543,15 +2561,6 @@ "text": "boolean" } }, - { - "name": "drag-handle", - "fieldName": "dragHandle", - "default": "false", - "description": "If true, renders a drag handle icon at the start of the tree item.", - "type": { - "text": "boolean" - } - }, { "name": "has-subtree", "fieldName": "hasSubtree", @@ -2582,7 +2591,7 @@ "default": "'sm'", "description": "The size of the tree item icons and actions.", "type": { - "text": "'xs' | 'sm' | 'md'" + "text": "ModusSize" } }, { @@ -2609,9 +2618,17 @@ "kind": "field", "name": "itemSelect", "type": { - "text": "EventEmitter<{\r\n value: string;\r\n }>" + "text": "EventEmitter<{\r\n value: string;\r\n selected?: boolean;\r\n }>" }, "description": "Event emitted when a tree item is selected." + }, + { + "kind": "field", + "name": "selectionsChange", + "type": { + "text": "EventEmitter<{\r\n selectedValues: string[];\r\n }>" + }, + "description": "Event emitted when checkbox selection changes in multi-select mode." } ], "customElement": true @@ -2653,6 +2670,18 @@ }, "description": "Reference to the host element" }, + { + "kind": "method", + "name": "handleItemSelect", + "parameters": [ + { + "name": "event", + "type": { + "text": "CustomEvent<{ value: string }>" + } + } + ] + }, { "kind": "method", "name": "render" From 42161a53bccc24d5e3d89bcf9ce54d8a43976c4c Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:46:02 +0530 Subject: [PATCH 14/39] 662 - fix bugs --- .../modus-wc-content-tree.scss | 2 +- .../modus-wc-content-tree.tsx | 56 ++++++++++--------- .../modus-wc-tree-actions.scss | 2 +- .../modus-wc-tree-actions.tsx | 6 +- .../modus-wc-tree-item/modus-wc-tree-item.tsx | 10 ++-- 5 files changed, 40 insertions(+), 36 deletions(-) diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index 75ce8b2dd0..53c3f2324d 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -19,8 +19,8 @@ modus-wc-content-tree { padding-bottom: var(--modus-wc-spacing-sm, 0.75rem); .modus-wc-content-tree-action-icon { - cursor: pointer; color: var(--modus-wc-color-black); + cursor: pointer; } } diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx index 8782ba1a3a..720d0b2887 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx @@ -84,37 +84,39 @@ export class ModusWcContentTree { } menuItems.forEach((item) => { - const label = item.getAttribute('label') || ''; - const normalizedLabel = label.toLowerCase(); - const matches = normalizedLabel.includes(normalizedSearch); + void (async () => { + const label = item.getAttribute('label') || ''; + const normalizedLabel = label.toLowerCase(); + const matches = normalizedLabel.includes(normalizedSearch); - if (matches) { - // Show matching node - (item as HTMLElement).style.display = ''; + if (matches) { + // Show matching node + (item as HTMLElement).style.display = ''; - // Expand and show all parent nodes - let parent = item.parentElement; - while (parent && parent !== this.el) { - if (parent.tagName === 'MODUS-WC-TREE-ITEM') { - parent.style.display = ''; - (parent as HTMLModusWcTreeItemElement).expandSubTree(); + // Expand and show all parent nodes + let parent = item.parentElement; + while (parent && parent !== this.el) { + if (parent.tagName === 'MODUS-WC-TREE-ITEM') { + parent.style.display = ''; + await (parent as HTMLModusWcTreeItemElement).expandSubTree(); + } + parent = parent.parentElement; } - parent = parent.parentElement; - } - } else { - // Check if any children match - const hasMatchingChildren = this.hasMatchingDescendants( - item as HTMLElement, - normalizedSearch - ); - - if (hasMatchingChildren) { - (item as HTMLElement).style.display = ''; - (item as HTMLModusWcTreeItemElement).expandSubTree(); } else { - (item as HTMLElement).style.display = 'none'; + // Check if any children match + const hasMatchingChildren = this.hasMatchingDescendants( + item as HTMLElement, + normalizedSearch + ); + + if (hasMatchingChildren) { + (item as HTMLElement).style.display = ''; + await (item as HTMLModusWcTreeItemElement).expandSubTree(); + } else { + (item as HTMLElement).style.display = 'none'; + } } - } + })(); }); } @@ -205,7 +207,7 @@ export class ModusWcContentTree { variant="borderless" size="sm" shape="circle" - onClick={this.toggleExpandCollapse} + onClick={() => void this.toggleExpandCollapse()} aria-label={ this.areAllExpanded ? 'Collapse all' : 'Expand all' } diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss index bdeaf18525..fd46b1f106 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss @@ -23,8 +23,8 @@ modus-wc-tree-actions { .modus-wc-tree-more-actions-dropdown { background: var(--modus-wc-color-base-page); - border-radius: 4px; border: 1px solid var(--modus-wc-color-base-100); + border-radius: 4px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); display: none; min-width: 150px; diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx index 6fb2e3b03c..dc245df596 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx @@ -2,13 +2,13 @@ import { createPopper, Instance as PopperInstance } from '@popperjs/core'; import { Component, Element, + EventEmitter, h, Host, + Listen, Prop, State, Event as StencilEvent, - EventEmitter, - Listen, } from '@stencil/core'; import { ModusSize } from '../../types'; @@ -105,7 +105,7 @@ export class ModusWcTreeActions { this.dropdownOpened.emit(this.el); if (this.popperInstance) { - this.popperInstance.update(); + void this.popperInstance.update(); } } }; diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx index aa27d720b1..168aa90100 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx @@ -9,10 +9,10 @@ import { State, Event as StencilEvent, } from '@stencil/core'; -import { Attributes, inheritAriaAttributes } from '../../utils'; import { convertPropsToClasses } from './modus-wc-tree-item.tailwind'; -import { ModusTreeItemActions } from '../modus-wc-tree-actions/modus-wc-tree-actions'; import { ModusSize } from '../../types'; +import { Attributes, inheritAriaAttributes } from '../../utils'; +import { ModusTreeItemActions } from '../modus-wc-tree-actions/modus-wc-tree-actions'; export interface IMenuItemElement extends HTMLElement { /** The unique identifying value of the tree item. */ @@ -104,7 +104,7 @@ export class ModusWcTreeItem { * Public method to collapse the subtree if it's expanded */ @Method() - async collapseSubTree(): Promise { + collapseSubTree(): Promise { if (this.hasSubtree && this.isExpanded) { const submenu = this.el.querySelector( '.modus-wc-tree-dropdown' @@ -115,13 +115,14 @@ export class ModusWcTreeItem { this.isExpanded = false; } } + return Promise.resolve(); } /** * Public method to expand the subtree if it's collapsed */ @Method() - async expandSubTree(): Promise { + expandSubTree(): Promise { if (this.hasSubtree && !this.isExpanded) { const submenu = this.el.querySelector( '.modus-wc-tree-dropdown' @@ -132,6 +133,7 @@ export class ModusWcTreeItem { this.isExpanded = true; } } + return Promise.resolve(); } private getClasses(): string { From 8174bb7a31d406f0eebdfc0fafa810f974b4af97 Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:28:34 +0530 Subject: [PATCH 15/39] 662 - add tests --- .../modus-wc-content-tree.spec.ts.snap | 49 +++++ .../modus-wc-content-tree.spec.ts | 169 ++++++++++++++++++ .../modus-wc-tree-actions.spec.ts.snap | 7 + .../modus-wc-tree-actions.spec.ts | 12 ++ .../modus-wc-tree-item.spec.ts.snap | 40 +++++ .../modus-wc-tree-item.spec.ts | 20 +++ .../modus-wc-tree-item/modus-wc-tree-item.tsx | 4 +- .../modus-wc-tree-view.spec.ts.snap | 17 ++ .../modus-wc-tree-view.spec.ts | 25 +++ 9 files changed, 341 insertions(+), 2 deletions(-) create mode 100644 src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap create mode 100644 src/components/modus-wc-content-tree/modus-wc-tree-actions/__snapshots__/modus-wc-tree-actions.spec.ts.snap create mode 100644 src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.spec.ts create mode 100644 src/components/modus-wc-content-tree/modus-wc-tree-item/__snapshots__/modus-wc-tree-item.spec.ts.snap create mode 100644 src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts create mode 100644 src/components/modus-wc-content-tree/modus-wc-tree-view/__snapshots__/modus-wc-tree-view.spec.ts.snap create mode 100644 src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts diff --git a/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap b/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap new file mode 100644 index 0000000000..108012a99a --- /dev/null +++ b/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`modus-wc-content-tree should render with custom props 1`] = ` + + +
    +
    + +
    + + + +
    +
    +
    +
    + + +
    +
    +
    +
    +`; + +exports[`modus-wc-content-tree should render with default props 1`] = ` + + +
    +
    + +
    + + + +
    +
    +
    +
    + + +
    +
    +
    +
    +`; diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts index e69de29bb2..d713dde342 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts @@ -0,0 +1,169 @@ +import { newSpecPage } from '@stencil/core/testing'; +import { ModusWcContentTree } from './modus-wc-content-tree'; + +describe('modus-wc-content-tree', () => { + it('should render with default props', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: '', + }); + expect(page.root).toMatchSnapshot(); + }); + + it('should render with custom props', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + `, + }); + expect(page.root).toMatchSnapshot(); + }); + + it('should filter nodes based on search input', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + Item 1 + Item 2 + Item 3 + `, + }); + + const searchInput = page.root!.querySelector('input[type="search"]'); + expect(searchInput as HTMLInputElement).toBeDefined(); + if (searchInput) { + (searchInput as HTMLInputElement).value = 'Item 2'; + searchInput.dispatchEvent(new Event('input')); + await page.waitForChanges(); + const visibleItems = page.root!.querySelectorAll( + 'modus-wc-tree-item:not([hidden])' + ); + expect(visibleItems.length).toBe(1); + expect(visibleItems[0].getAttribute('value')).toBe('item2'); + } + }); + + it('clears filter on Escape key', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + + + + + `, + }); + + const input = page.root!.querySelector('modus-wc-text-input')!; + + input.dispatchEvent( + new CustomEvent('inputChange', { + bubbles: true, + composed: true, + }) + ); + + await page.waitForChanges(); + + // Now press Escape + input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); + await page.waitForChanges(); + + // Assert all items visible + const items = page.root!.querySelectorAll('modus-wc-tree-item'); + items.forEach((item) => { + expect((item as HTMLElement).style.display).toBe(''); + }); + }); + + it('expands parent nodes when a child matches search', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + + + + + + `, + }); + + const root = page.root!; + const parent = root.querySelectorAll('modus-wc-tree-item')[0] as any; + const child = root.querySelectorAll('modus-wc-tree-item')[1] as HTMLElement; + + // Mock expandSubTree on parent + parent.expandSubTree = jest.fn().mockResolvedValue(undefined); + + const tree = page.rootInstance as any; + + // Trigger filter that matches child + tree.filterNodes('child'); + await page.waitForChanges(); + + // Child should be visible + expect(child.style.display).toBe(''); + + // Parent should be forced visible + expect(parent.style.display).toBe(''); + + // Parent expansion should be triggered + expect(parent.expandSubTree).toHaveBeenCalled(); + }); + + it('updateSlotContent returns early if slotEl is not set', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ``, + }); + + const tree = page.rootInstance as any; + + tree.slotEl = undefined; + + // Should not throw + tree.updateSlotContent(); + + expect(tree.hasSlotContent).toBe(false); + }); + + it('detects subtree via property instead of attribute', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + + + + `, + }); + + const tree = page.rootInstance as any; + const item = page.root!.querySelector('modus-wc-tree-item') as any; + + item.hasSubtree = true; + item.expandSubTree = jest.fn().mockResolvedValue(undefined); + + await tree.toggleExpandCollapse(); + + expect(item.expandSubTree).toHaveBeenCalled(); + }); + + it('skips nodes without subtrees', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + + + + `, + }); + + const tree = page.rootInstance as any; + + await expect(tree.toggleExpandCollapse()).resolves.not.toThrow(); + }); +}); diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/__snapshots__/modus-wc-tree-actions.spec.ts.snap b/src/components/modus-wc-content-tree/modus-wc-tree-actions/__snapshots__/modus-wc-tree-actions.spec.ts.snap new file mode 100644 index 0000000000..160617783f --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/__snapshots__/modus-wc-tree-actions.spec.ts.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`modus-wc-tree-actions renders correctly 1`] = ` + +
    +
    +`; diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.spec.ts new file mode 100644 index 0000000000..47fe5287ba --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.spec.ts @@ -0,0 +1,12 @@ +import { newSpecPage } from '@stencil/core/testing'; +import { ModusWcTreeActions } from './modus-wc-tree-actions'; + +describe('modus-wc-tree-actions', () => { + it('renders correctly', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + expect(page.root).toMatchSnapshot(); + }); +}); diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/__snapshots__/modus-wc-tree-item.spec.ts.snap b/src/components/modus-wc-content-tree/modus-wc-tree-item/__snapshots__/modus-wc-tree-item.spec.ts.snap new file mode 100644 index 0000000000..2beb290990 --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/__snapshots__/modus-wc-tree-item.spec.ts.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`modus-wc-tree-item renders with checkbox 1`] = ` + + +
  • +
    + + +
    +
    + Test Item +
    +
    +
    + +
    +
    +
  • +
    +`; + +exports[`modus-wc-tree-item renders with default props 1`] = ` + + +
  • +
    + +
    +
    + Test Item +
    +
    +
    + +
    +
    +
  • +
    +`; diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts new file mode 100644 index 0000000000..882de23eec --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts @@ -0,0 +1,20 @@ +import { newSpecPage } from '@stencil/core/testing'; +import { ModusWcTreeItem } from './modus-wc-tree-item'; + +describe('modus-wc-tree-item', () => { + it('renders with default props', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + expect(page.root).toMatchSnapshot(); + }); + + it('renders with checkbox', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + expect(page.root).toMatchSnapshot(); + }); +}); diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx index 168aa90100..67bfe632f5 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx @@ -92,12 +92,12 @@ export class ModusWcTreeItem { componentDidLoad() { if (this.hasSubtree) { - this.el.addEventListener('itemSelect', this.updateIndeterminateState); + this.el.addEventListener('selectionsChange', this.updateIndeterminateState); } } disconnectedCallback() { - this.el.removeEventListener('itemSelect', this.updateIndeterminateState); + this.el.removeEventListener('selectionsChange', this.updateIndeterminateState); } /** diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/__snapshots__/modus-wc-tree-view.spec.ts.snap b/src/components/modus-wc-content-tree/modus-wc-tree-view/__snapshots__/modus-wc-tree-view.spec.ts.snap new file mode 100644 index 0000000000..4c1a52e73b --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/__snapshots__/modus-wc-tree-view.spec.ts.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`modus-wc-tree-view renders with custom props 1`] = ` + + +
      +
    +
    +`; + +exports[`modus-wc-tree-view renders with default props 1`] = ` + + +
      +
      +`; diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts new file mode 100644 index 0000000000..1f9305a165 --- /dev/null +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts @@ -0,0 +1,25 @@ +import { newSpecPage } from '@stencil/core/testing'; +import { ModusWcTreeView } from './modus-wc-tree-view'; + +describe('modus-wc-tree-view', () => { + it('renders with default props', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeView], + html: ``, + }); + expect(page.root).toMatchSnapshot(); + }); + + it('renders with custom props', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeView], + html: ` + `, + }); + expect(page.root).toMatchSnapshot(); + }); +}); From 2bf2b91fbfd9378d7c2124538b3a5ba4f921795a Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:15:21 +0530 Subject: [PATCH 16/39] 662 - build fixes --- .../modus-wc-content-tree.spec.ts | 87 ------------------- 1 file changed, 87 deletions(-) diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts index d713dde342..7eafb7784b 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts @@ -79,91 +79,4 @@ describe('modus-wc-content-tree', () => { expect((item as HTMLElement).style.display).toBe(''); }); }); - - it('expands parent nodes when a child matches search', async () => { - const page = await newSpecPage({ - components: [ModusWcContentTree], - html: ` - - - - - - `, - }); - - const root = page.root!; - const parent = root.querySelectorAll('modus-wc-tree-item')[0] as any; - const child = root.querySelectorAll('modus-wc-tree-item')[1] as HTMLElement; - - // Mock expandSubTree on parent - parent.expandSubTree = jest.fn().mockResolvedValue(undefined); - - const tree = page.rootInstance as any; - - // Trigger filter that matches child - tree.filterNodes('child'); - await page.waitForChanges(); - - // Child should be visible - expect(child.style.display).toBe(''); - - // Parent should be forced visible - expect(parent.style.display).toBe(''); - - // Parent expansion should be triggered - expect(parent.expandSubTree).toHaveBeenCalled(); - }); - - it('updateSlotContent returns early if slotEl is not set', async () => { - const page = await newSpecPage({ - components: [ModusWcContentTree], - html: ``, - }); - - const tree = page.rootInstance as any; - - tree.slotEl = undefined; - - // Should not throw - tree.updateSlotContent(); - - expect(tree.hasSlotContent).toBe(false); - }); - - it('detects subtree via property instead of attribute', async () => { - const page = await newSpecPage({ - components: [ModusWcContentTree], - html: ` - - - - `, - }); - - const tree = page.rootInstance as any; - const item = page.root!.querySelector('modus-wc-tree-item') as any; - - item.hasSubtree = true; - item.expandSubTree = jest.fn().mockResolvedValue(undefined); - - await tree.toggleExpandCollapse(); - - expect(item.expandSubTree).toHaveBeenCalled(); - }); - - it('skips nodes without subtrees', async () => { - const page = await newSpecPage({ - components: [ModusWcContentTree], - html: ` - - - - `, - }); - - const tree = page.rootInstance as any; - - await expect(tree.toggleExpandCollapse()).resolves.not.toThrow(); - }); }); From ee19ec0582788e944ff7f5ad27a024056080cd44 Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:21:23 +0530 Subject: [PATCH 17/39] 662 - build fixes --- .../modus-wc-tree-item/modus-wc-tree-item.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx index 67bfe632f5..e871c4fb3e 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx @@ -92,12 +92,18 @@ export class ModusWcTreeItem { componentDidLoad() { if (this.hasSubtree) { - this.el.addEventListener('selectionsChange', this.updateIndeterminateState); + this.el.addEventListener( + 'selectionsChange', + this.updateIndeterminateState + ); } } disconnectedCallback() { - this.el.removeEventListener('selectionsChange', this.updateIndeterminateState); + this.el.removeEventListener( + 'selectionsChange', + this.updateIndeterminateState + ); } /** From f42040183af3c266da151e55e6ffaeda6de6c5b0 Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:06:51 +0530 Subject: [PATCH 18/39] 662 - add tests --- src/components.d.ts | 4 - .../modus-wc-content-tree.spec.ts.snap | 4 +- .../modus-wc-content-tree.spec.ts | 643 ++++++++++++++ .../modus-wc-content-tree.tsx | 144 ++-- .../modus-wc-tree-actions.spec.ts | 596 +++++++++++++ .../modus-wc-tree-item.spec.ts | 815 +++++++++++++++++- .../modus-wc-tree-item/modus-wc-tree-item.tsx | 24 +- .../modus-wc-tree-view.spec.ts.snap | 12 +- .../modus-wc-tree-view.spec.ts | 226 +++++ .../modus-wc-content-tree/readme.md | 1 - src/custom-elements.json | 2 +- 11 files changed, 2385 insertions(+), 86 deletions(-) diff --git a/src/components.d.ts b/src/components.d.ts index e8d31be81b..1164e36207 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -519,7 +519,6 @@ export namespace Components { } /** * A customizable content tree component used to display hierarchical data in a tree structure. - * Uses menu items to create the tree structure with support for expanding/collapsing nodes and selection. */ interface ModusWcContentTree { /** @@ -2508,7 +2507,6 @@ declare global { }; /** * A customizable content tree component used to display hierarchical data in a tree structure. - * Uses menu items to create the tree structure with support for expanding/collapsing nodes and selection. */ interface HTMLModusWcContentTreeElement extends Components.ModusWcContentTree, HTMLStencilElement { } @@ -3826,7 +3824,6 @@ declare namespace LocalJSX { } /** * A customizable content tree component used to display hierarchical data in a tree structure. - * Uses menu items to create the tree structure with support for expanding/collapsing nodes and selection. */ interface ModusWcContentTree { /** @@ -5845,7 +5842,6 @@ declare module "@stencil/core" { "modus-wc-collapse": LocalJSX.ModusWcCollapse & JSXBase.HTMLAttributes; /** * A customizable content tree component used to display hierarchical data in a tree structure. - * Uses menu items to create the tree structure with support for expanding/collapsing nodes and selection. */ "modus-wc-content-tree": LocalJSX.ModusWcContentTree & JSXBase.HTMLAttributes; /** diff --git a/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap b/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap index 108012a99a..5f26a01fc5 100644 --- a/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap +++ b/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap @@ -14,7 +14,7 @@ exports[`modus-wc-content-tree should render with custom props 1`] = `
      -
      +
      @@ -38,7 +38,7 @@ exports[`modus-wc-content-tree should render with default props 1`] = `
      -
      +
      diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts index 7eafb7784b..0817c31457 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts @@ -1,5 +1,6 @@ import { newSpecPage } from '@stencil/core/testing'; import { ModusWcContentTree } from './modus-wc-content-tree'; +import { ModusWcTreeItemElement } from './modus-wc-tree-item/modus-wc-tree-item'; describe('modus-wc-content-tree', () => { it('should render with default props', async () => { @@ -79,4 +80,646 @@ describe('modus-wc-content-tree', () => { expect((item as HTMLElement).style.display).toBe(''); }); }); + + it('expands parent nodes when a child matches search', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + + + + + + `, + }); + + const root = page.root!; + const parent = root.querySelectorAll( + 'modus-wc-tree-item' + )[0] as ModusWcTreeItemElement; + const child = root.querySelectorAll( + 'modus-wc-tree-item' + )[1] as ModusWcTreeItemElement; + + // Mock expandSubTree on parent + parent.expandSubTree = jest.fn().mockResolvedValue(undefined); + + const tree = page.rootInstance; + + // Trigger filter that matches child + tree.filterNodes('child'); + await page.waitForChanges(); + + // Child should be visible + expect(child.style.display).toBe(''); + + // Parent should be forced visible + expect(parent.style.display).toBe(''); + + // Parent expansion should be triggered + expect(parent.expandSubTree).toHaveBeenCalled(); + }); + + it('updateSlotContent returns early if slotEl is not set', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ``, + }); + + const tree = page.rootInstance; + + tree.slotEl = undefined; + + // Should not throw + tree.updateSlotContent(); + + expect(tree.hasSlotContent).toBe(false); + }); + + it('detects subtree via property instead of attribute', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + + + + `, + }); + + const tree = page.rootInstance; + const item = page.root!.querySelector( + 'modus-wc-tree-item' + ) as ModusWcTreeItemElement; + + item.hasSubtree = true; + item.expandSubTree = jest.fn().mockResolvedValue(undefined); + + await tree.toggleExpandCollapse(); + + expect(item.expandSubTree).toHaveBeenCalled(); + }); + + it('skips nodes without subtrees', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + + + + `, + }); + + const tree = page.rootInstance; + + await expect(tree.toggleExpandCollapse()).resolves.not.toThrow(); + }); + + it('removes event listener on disconnectedCallback', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: '', + }); + + const tree = page.rootInstance; + const mockRemoveEventListener = jest.fn(); + + // Set up slotEl with mock removeEventListener + tree.slotEl = { + removeEventListener: mockRemoveEventListener, + } as unknown as HTMLSlotElement; + + // Call disconnectedCallback + tree.disconnectedCallback(); + + // Verify removeEventListener was called with correct arguments + expect(mockRemoveEventListener).toHaveBeenCalledWith( + 'slotchange', + tree.updateSlotContent + ); + }); + + it('disconnectedCallback handles missing slotEl gracefully', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: '', + }); + + const tree = page.rootInstance; + tree.slotEl = undefined; + + // Should not throw when slotEl is undefined + expect(() => tree.disconnectedCallback()).not.toThrow(); + }); + + it('clears debounce timer on disconnectedCallback', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: '', + }); + + const tree = page.rootInstance; + const clearTimeoutSpy = jest.spyOn(window, 'clearTimeout'); + + // Set up a mock debounce timer + tree['debounceTimer'] = 123 as unknown as number; + + // Call disconnectedCallback + tree.disconnectedCallback(); + + // Verify clearTimeout was called with the timer + expect(clearTimeoutSpy).toHaveBeenCalledWith(123); + + clearTimeoutSpy.mockRestore(); + }); + + it('disconnectedCallback handles missing debounceTimer gracefully', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: '', + }); + + const tree = page.rootInstance; + const clearTimeoutSpy = jest.spyOn(window, 'clearTimeout'); + + // Ensure debounceTimer is undefined + tree['debounceTimer'] = undefined; + + // Should not throw and should not call clearTimeout + expect(() => tree.disconnectedCallback()).not.toThrow(); + expect(clearTimeoutSpy).not.toHaveBeenCalled(); + + clearTimeoutSpy.mockRestore(); + }); + + it('clears both event listener and timer on disconnectedCallback', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: '', + }); + + const tree = page.rootInstance; + const mockRemoveEventListener = jest.fn(); + const clearTimeoutSpy = jest.spyOn(window, 'clearTimeout'); + + // Set up both slotEl and debounceTimer + tree.slotEl = { + removeEventListener: mockRemoveEventListener, + } as unknown as HTMLSlotElement; + tree['debounceTimer'] = 456 as unknown as number; + + // Call disconnectedCallback + tree.disconnectedCallback(); + + // Verify both cleanup operations were performed + expect(mockRemoveEventListener).toHaveBeenCalledWith( + 'slotchange', + tree.updateSlotContent + ); + expect(clearTimeoutSpy).toHaveBeenCalledWith(456); + + clearTimeoutSpy.mockRestore(); + }); + + it('renders without search when includeSearch is false', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: '', + }); + + const searchInput = page.root?.querySelector('modus-wc-text-input'); + expect(searchInput).toBeNull(); + }); + + it('renders without actions when includeActions is false', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: '', + }); + + const actionsDiv = page.root?.querySelector( + '.modus-wc-content-tree-actions' + ); + expect(actionsDiv).toBeNull(); + }); + + it('applies custom class to wrapper', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: '', + }); + + const wrapper = page.root?.querySelector('.modus-wc-content-tree-wrapper'); + expect(wrapper?.className).toContain('my-custom-class'); + }); + + it('uses custom search placeholder', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: '', + }); + + const searchInput = page.root?.querySelector('modus-wc-text-input'); + expect(searchInput?.getAttribute('placeholder')).toBe('Find items...'); + }); + + it('shows empty state when hasSlotContent is false', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: '', + }); + + const tree = page.rootInstance; + tree.hasSlotContent = false; + await page.waitForChanges(); + + const emptyState = page.root?.querySelector('.modus-wc-content-tree-empty'); + expect(emptyState).toBeDefined(); + + const emptyIcon = emptyState?.querySelector('modus-wc-icon'); + expect(emptyIcon?.getAttribute('name')).toBe('folder_open'); + + const emptyText = emptyState?.querySelector('modus-wc-typography'); + expect(emptyText?.getAttribute('label')).toBe('Empty Content Tree'); + }); + + it('hides empty state when hasSlotContent is true', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + + + + `, + }); + + const tree = page.rootInstance; + tree.hasSlotContent = true; + await page.waitForChanges(); + + const emptyState = page.root?.querySelector('.modus-wc-content-tree-empty'); + expect(emptyState).toBeNull(); + }); + + it('renders expand/collapse button with "Expand all" aria-label when collapsed', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: '', + }); + + const tree = page.rootInstance; + tree.areAllExpanded = false; + await page.waitForChanges(); + + const button = page.root?.querySelector( + '.modus-wc-content-tree-actions modus-wc-button' + ); + expect(button?.getAttribute('aria-label')).toBe('Expand all'); + expect(tree.areAllExpanded).toBe(false); + }); + + it('renders expand/collapse button with "Collapse all" aria-label when expanded', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: '', + }); + + const tree = page.rootInstance; + tree.areAllExpanded = true; + await page.waitForChanges(); + + const button = page.root?.querySelector( + '.modus-wc-content-tree-actions modus-wc-button' + ); + expect(button?.getAttribute('aria-label')).toBe('Collapse all'); + expect(tree.areAllExpanded).toBe(true); + }); + + it('componentWillLoad sets hasSlotContent based on child nodes', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + + + + `, + }); + + expect(page.rootInstance.hasSlotContent).toBe(true); + }); + + it('componentWillLoad filters out STYLE nodes', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + + + + `, + }); + + expect(page.rootInstance.hasSlotContent).toBe(false); + }); + + it('componentDidLoad handles missing slotEl gracefully', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: '', + }); + + const tree = page.rootInstance; + tree.slotEl = undefined; + + // Should not throw when slotEl is undefined due to optional chaining + expect(() => tree.componentDidLoad()).not.toThrow(); + }); + + it('componentDidLoad adds addEventListener when slotEl exists', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: '
      Content
      ', + }); + + const tree = page.rootInstance; + + // Create a mock slot element with addEventListener spy + const addEventListenerSpy = jest.fn(); + const mockSlot = { + addEventListener: addEventListenerSpy, + assignedNodes: jest + .fn() + .mockReturnValue([{ nodeType: Node.ELEMENT_NODE, tagName: 'DIV' }]), + } as unknown as HTMLSlotElement; + + // Mock querySelector to return our mock slot + jest.spyOn(tree.el, 'querySelector').mockReturnValue(mockSlot); + + // Call componentDidLoad + tree.componentDidLoad(); + + // Verify addEventListener was called + expect(addEventListenerSpy).toHaveBeenCalledWith( + 'slotchange', + tree.updateSlotContent + ); + }); + + it('filterNodes catches and handles expandSubTree errors gracefully', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + + + + + + `, + }); + + const tree = page.rootInstance; + const parent = page.root?.querySelector( + 'modus-wc-tree-item' + ) as ModusWcTreeItemElement; + + // Mock expandSubTree to reject with error + parent.expandSubTree = jest + .fn() + .mockRejectedValue(new Error('Expand failed')); + + // Should not throw despite the error + await expect(tree.filterNodes('Child')).resolves.not.toThrow(); + + expect(parent.expandSubTree).toHaveBeenCalled(); + }); + + it('filterNodes continues expanding other items when one fails', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + + + + + + + + + `, + }); + + const tree = page.rootInstance; + const parents = page.root?.querySelectorAll( + 'modus-wc-tree-item[has-subtree]' + ) as NodeListOf; + + // First parent fails + parents[0].expandSubTree = jest.fn().mockRejectedValue(new Error('Failed')); + + // Second parent succeeds + parents[1].expandSubTree = jest.fn().mockResolvedValue(undefined); + + await tree.filterNodes('Match'); + + // Both should be called despite first one failing + expect(parents[0].expandSubTree).toHaveBeenCalled(); + expect(parents[1].expandSubTree).toHaveBeenCalled(); + }); + + it('updateSlotContent invalidates cached items', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: '', + }); + + const tree = page.rootInstance; + tree['cachedItems'] = [] as any; + + const mockSlot = { + assignedNodes: jest.fn().mockReturnValue([]), + } as unknown as HTMLSlotElement; + + tree.slotEl = mockSlot; + tree.updateSlotContent(); + + expect(tree['cachedItems']).toBeUndefined(); + }); + + it('updateSlotContent filters out STYLE nodes from assigned nodes', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: '', + }); + + const tree = page.rootInstance; + const styleNode = { nodeType: Node.ELEMENT_NODE, tagName: 'STYLE' }; + const elementNode = { nodeType: Node.ELEMENT_NODE, tagName: 'DIV' }; + + const mockSlot = { + assignedNodes: jest.fn().mockReturnValue([styleNode, elementNode]), + } as unknown as HTMLSlotElement; + + tree.slotEl = mockSlot; + tree.updateSlotContent(); + + expect(tree.hasSlotContent).toBe(true); + }); + + it('toggleExpandCollapse collapses when areAllExpanded is true', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + + + + `, + }); + + const tree = page.rootInstance; + tree.areAllExpanded = true; + + const item = page.root?.querySelector( + 'modus-wc-tree-item' + ) as ModusWcTreeItemElement; + item.collapseSubTree = jest.fn().mockResolvedValue(undefined); + + await tree.toggleExpandCollapse(); + + expect(item.collapseSubTree).toHaveBeenCalled(); + expect(tree.areAllExpanded).toBe(false); + }); + + it('filterNodes caches menu items on first call', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + + + + + `, + }); + + const tree = page.rootInstance; + expect(tree['cachedItems']).toBeUndefined(); + + await tree.filterNodes('Item'); + + expect(tree['cachedItems']).toBeDefined(); + expect(tree['cachedItems']?.length).toBe(2); + }); + + it('filterNodes hides non-matching items without matching descendants', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + + + + + `, + }); + + const tree = page.rootInstance; + await tree.filterNodes('Alpha'); + + const items = page.root?.querySelectorAll('modus-wc-tree-item'); + expect((items?.[0] as HTMLElement).style.display).toBe(''); + expect((items?.[1] as HTMLElement).style.display).toBe('none'); + }); + + it('filterNodes shows items with matching descendants', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + + + + + + `, + }); + + const tree = page.rootInstance; + const parent = page.root?.querySelector( + 'modus-wc-tree-item' + ) as ModusWcTreeItemElement; + parent.expandSubTree = jest.fn().mockResolvedValue(undefined); + + await tree.filterNodes('Child'); + + expect((parent as HTMLElement).style.display).toBe(''); + expect(parent.expandSubTree).toHaveBeenCalled(); + }); + + it('inherits ARIA attributes', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: '', + }); + + const wrapper = page.root?.querySelector('.modus-wc-content-tree-wrapper'); + expect(wrapper?.getAttribute('aria-label')).toBe('My Tree'); + }); + + it('filterNodes handles items without label attribute', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + + + + + `, + }); + + const tree = page.rootInstance; + await tree.filterNodes('search'); + + const items = page.root?.querySelectorAll('modus-wc-tree-item'); + // Item without label should be hidden (empty string doesn't match) + expect((items?.[1] as HTMLElement).style.display).toBe('none'); + }); + + it('should debounce input without timeouts', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ``, + }); + + const instance = page.rootInstance as any; + + const filterSpy = jest + .spyOn(instance, 'filterNodes') + .mockResolvedValue(undefined); + + instance.handleInputChange({ + target: { value: 'hello' }, + } as any); + + // Wait slightly longer than debounce (150ms) + await new Promise((r) => setTimeout(r, 180)); + + expect(filterSpy).toHaveBeenCalledWith('hello'); + }); + + it('covers clearTimeout branch', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ``, + }); + + const instance = page.rootInstance as any; + + jest.spyOn(instance, 'filterNodes').mockResolvedValue(undefined); + + const clearSpy = jest.spyOn(window, 'clearTimeout'); + + // Force branch + instance.debounceTimer = 999; + + instance.handleInputChange({ + target: { value: 'x' }, + } as any); + + expect(clearSpy).toHaveBeenCalledWith(999); + }); }); diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx index 720d0b2887..53e9a37d0c 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx @@ -1,15 +1,9 @@ import { Component, Element, h, Host, Prop, State } from '@stencil/core'; import { Attributes, inheritAriaAttributes } from '../utils'; - -interface HTMLModusWcTreeItemElement extends HTMLElement { - hasSubtree?: boolean; - expandSubTree: () => Promise; - collapseSubTree: () => Promise; -} +import { ModusWcTreeItemElement } from './modus-wc-tree-item/modus-wc-tree-item'; /** * A customizable content tree component used to display hierarchical data in a tree structure. - * Uses menu items to create the tree structure with support for expanding/collapsing nodes and selection. */ @Component({ tag: 'modus-wc-content-tree', @@ -19,6 +13,8 @@ interface HTMLModusWcTreeItemElement extends HTMLElement { export class ModusWcContentTree { private inheritedAttributes: Attributes = {}; private slotEl?: HTMLSlotElement; + private debounceTimer?: number; + private cachedItems?: HTMLModusWcTreeItemElement[]; /** Reference to the host element */ @Element() el!: HTMLElement; @@ -63,77 +59,100 @@ export class ModusWcContentTree { disconnectedCallback() { this.slotEl?.removeEventListener('slotchange', this.updateSlotContent); + if (this.debounceTimer) { + window.clearTimeout(this.debounceTimer); + } } private handleInputChange = (event: CustomEvent) => { const target = event.target as HTMLInputElement; this.searchValue = target.value; - this.filterNodes(this.searchValue); + + // Debounce search to avoid excessive filtering + if (this.debounceTimer) { + window.clearTimeout(this.debounceTimer); + } + + this.debounceTimer = window.setTimeout(() => { + this.filterNodes(this.searchValue); + }, 150); }; - private filterNodes(searchTerm: string) { - const menuItems = this.el.querySelectorAll('modus-wc-tree-item'); + private async filterNodes(searchTerm: string): Promise { + // Cache menu items to avoid repeated queries + if (!this.cachedItems) { + this.cachedItems = Array.from( + this.el.querySelectorAll('modus-wc-tree-item') + ) as HTMLModusWcTreeItemElement[]; + } + const menuItems = this.cachedItems; + const normalizedSearch = searchTerm.toLowerCase().trim(); + // If search is empty, reset everything in batch if (!normalizedSearch) { - // Show all nodes when search is empty - menuItems.forEach((item) => { + for (const item of menuItems) { (item as HTMLElement).style.display = ''; - }); + } return; } - menuItems.forEach((item) => { - void (async () => { - const label = item.getAttribute('label') || ''; - const normalizedLabel = label.toLowerCase(); - const matches = normalizedLabel.includes(normalizedSearch); - - if (matches) { - // Show matching node - (item as HTMLElement).style.display = ''; - - // Expand and show all parent nodes - let parent = item.parentElement; - while (parent && parent !== this.el) { - if (parent.tagName === 'MODUS-WC-TREE-ITEM') { - parent.style.display = ''; - await (parent as HTMLModusWcTreeItemElement).expandSubTree(); - } - parent = parent.parentElement; + // First pass: identify matches and collect items to show/hide + const matchingItems = new Set(); + const itemsToExpand: HTMLModusWcTreeItemElement[] = []; + const processedParents = new Set(); + + // Build match set efficiently + for (const item of menuItems) { + const label = item.getAttribute('label') || ''; + if (label.toLowerCase().includes(normalizedSearch)) { + matchingItems.add(item as HTMLElement); + } + } + + // Second pass: determine visibility and expansion needs + for (const item of menuItems) { + const itemElement = item as HTMLElement; + const isDirectMatch = matchingItems.has(itemElement); + + if (isDirectMatch) { + itemElement.style.display = ''; + + // Process ancestors only once + let parent = item.parentElement; + while (parent && parent !== this.el) { + if ( + parent.tagName === 'MODUS-WC-TREE-ITEM' && + !processedParents.has(parent) + ) { + processedParents.add(parent); + parent.style.display = ''; + itemsToExpand.push(parent as HTMLModusWcTreeItemElement); } + parent = parent.parentElement; + } + } else { + // Check descendants using pre-computed match set + const descendants = itemElement.querySelectorAll('modus-wc-tree-item'); + const hasMatchingChild = Array.from(descendants).some((child) => + matchingItems.has(child as HTMLElement) + ); + + if (hasMatchingChild) { + itemElement.style.display = ''; + itemsToExpand.push(item); } else { - // Check if any children match - const hasMatchingChildren = this.hasMatchingDescendants( - item as HTMLElement, - normalizedSearch - ); - - if (hasMatchingChildren) { - (item as HTMLElement).style.display = ''; - await (item as HTMLModusWcTreeItemElement).expandSubTree(); - } else { - (item as HTMLElement).style.display = 'none'; - } + itemElement.style.display = 'none'; } - })(); - }); - } - - private hasMatchingDescendants( - element: HTMLElement, - searchTerm: string - ): boolean { - const childMenuItems = element.querySelectorAll('modus-wc-tree-item'); - - for (const child of Array.from(childMenuItems)) { - const label = child.getAttribute('label') || ''; - if (label.toLowerCase().includes(searchTerm)) { - return true; } } - return false; + // Batch all expand operations + if (itemsToExpand.length > 0) { + await Promise.all( + itemsToExpand.map((item) => item.expandSubTree().catch(() => {})) + ); + } } private handleInputKeyDown = (event: KeyboardEvent) => { @@ -155,6 +174,8 @@ export class ModusWcContentTree { ); this.hasSlotContent = assigned.length > 0; + // Invalidate cache when content changes + this.cachedItems = undefined; }; private toggleExpandCollapse = async () => { @@ -162,10 +183,9 @@ export class ModusWcContentTree { this.areAllExpanded = !this.areAllExpanded; const promises = Array.from(treeItems).map((item) => { - const treeItem = item as HTMLModusWcTreeItemElement; + const treeItem = item as ModusWcTreeItemElement; const hasSubtree = item.hasAttribute('has-subtree') || treeItem.hasSubtree === true; - if (hasSubtree) { if (this.areAllExpanded) { return treeItem.expandSubTree(); @@ -207,7 +227,7 @@ export class ModusWcContentTree { variant="borderless" size="sm" shape="circle" - onClick={() => void this.toggleExpandCollapse()} + onClick={this.toggleExpandCollapse} aria-label={ this.areAllExpanded ? 'Collapse all' : 'Expand all' } @@ -223,7 +243,7 @@ export class ModusWcContentTree {
      )}
      -
      +
      {!this.hasSlotContent && (
      diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.spec.ts index 47fe5287ba..2edfc87c87 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.spec.ts @@ -9,4 +9,600 @@ describe('modus-wc-tree-actions', () => { }); expect(page.root).toMatchSnapshot(); }); + + it('renders with single action', async () => { + const actions = [{ id: '1', icon: 'edit', label: 'Edit' }]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + const buttons = page.root?.querySelectorAll('modus-wc-button'); + expect(buttons?.length).toBe(1); + }); + + it('emits treeActionClick when action is clicked', async () => { + const actions = [{ id: 'edit-1', icon: 'edit', label: 'Edit' }]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + const eventSpy = jest.fn(); + page.root?.addEventListener('treeActionClick', eventSpy); + + const button = page.root?.querySelector('modus-wc-button'); + button?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await page.waitForChanges(); + + expect(eventSpy).toHaveBeenCalled(); + expect(eventSpy.mock.calls[0][0].detail.actionId).toBe('edit-1'); + expect(eventSpy.mock.calls[0][0].detail.actionName).toBe('Edit'); + }); + + it('does not emit event when disabled action is clicked', async () => { + const actions = [{ id: '1', icon: 'edit', label: 'Edit', disabled: true }]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + const eventSpy = jest.fn(); + page.root?.addEventListener('treeActionClick', eventSpy); + + const treeActions = page.rootInstance; + treeActions['handleActionClick']( + actions[0], + new MouseEvent('click') as any + ); + + expect(eventSpy).not.toHaveBeenCalled(); + }); + + it('toggles dropdown on more actions button click', async () => { + const actions = [ + { id: '1', icon: 'edit', label: 'Edit' }, + { id: '2', icon: 'delete', label: 'Delete' }, + { id: '3', icon: 'share', label: 'Share' }, + ]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + const treeActions = page.rootInstance; + expect(treeActions.isDropdownOpen).toBe(false); + + treeActions['handleMoreActionsClick'](new MouseEvent('click') as any); + await page.waitForChanges(); + + expect(treeActions.isDropdownOpen).toBe(true); + + treeActions['handleMoreActionsClick'](new MouseEvent('click') as any); + await page.waitForChanges(); + + expect(treeActions.isDropdownOpen).toBe(false); + }); + + it('emits dropdownOpened when dropdown is opened', async () => { + const actions = [ + { id: '1', icon: 'edit', label: 'Edit' }, + { id: '2', icon: 'delete', label: 'Delete' }, + ]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + const eventSpy = jest.fn(); + page.root?.addEventListener('dropdownOpened', eventSpy); + + const treeActions = page.rootInstance; + treeActions['handleMoreActionsClick'](new MouseEvent('click') as any); + await page.waitForChanges(); + + expect(eventSpy).toHaveBeenCalled(); + }); + + it('closes dropdown when clicking outside', async () => { + const actions = [ + { id: '1', icon: 'edit', label: 'Edit' }, + { id: '2', icon: 'delete', label: 'Delete' }, + ]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + const treeActions = page.rootInstance; + treeActions.isDropdownOpen = true; + + const outsideElement = document.createElement('div'); + const clickEvent = new MouseEvent('click', { bubbles: true }); + Object.defineProperty(clickEvent, 'target', { + value: outsideElement, + enumerable: true, + }); + + treeActions['handleClickOutside'](clickEvent); + await page.waitForChanges(); + + expect(treeActions.isDropdownOpen).toBe(false); + }); + + it('closes dropdown when moreActionsButton ref is not available', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + const treeActions = page.rootInstance; + treeActions.isDropdownOpen = true; + treeActions.moreActionsButton = null as any; + + const outsideElement = document.createElement('div'); + const clickEvent = new MouseEvent('click', { bubbles: true }); + Object.defineProperty(clickEvent, 'target', { + value: outsideElement, + enumerable: true, + }); + + treeActions['handleClickOutside'](clickEvent); + + expect(treeActions.isDropdownOpen).toBe(false); + }); + + it('does not process click outside when dropdown is closed', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + const treeActions = page.rootInstance; + treeActions.isDropdownOpen = false; + + const outsideElement = document.createElement('div'); + const clickEvent = new MouseEvent('click', { bubbles: true }); + Object.defineProperty(clickEvent, 'target', { + value: outsideElement, + enumerable: true, + }); + + const initialState = treeActions.isDropdownOpen; + treeActions['handleClickOutside'](clickEvent); + + expect(treeActions.isDropdownOpen).toBe(initialState); + }); + + it('closes dropdown when another dropdown is opened', async () => { + const actions = [ + { id: '1', icon: 'edit', label: 'Edit' }, + { id: '2', icon: 'delete', label: 'Delete' }, + ]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + const treeActions = page.rootInstance; + treeActions.isDropdownOpen = true; + + const otherElement = document.createElement('div'); + const event = new CustomEvent('dropdownOpened', { detail: otherElement }); + + treeActions.handleOtherDropdownOpened(event); + await page.waitForChanges(); + + expect(treeActions.isDropdownOpen).toBe(false); + }); + + it('does not close dropdown when same dropdown is opened', async () => { + const actions = [ + { id: '1', icon: 'edit', label: 'Edit' }, + { id: '2', icon: 'delete', label: 'Delete' }, + ]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + const treeActions = page.rootInstance; + treeActions.isDropdownOpen = true; + + const event = new CustomEvent('dropdownOpened', { detail: page.root }); + + treeActions.handleOtherDropdownOpened(event); + + expect(treeActions.isDropdownOpen).toBe(true); + }); + + it('adds click event listener on componentDidLoad', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + const addEventListenerSpy = jest.spyOn(document, 'addEventListener'); + + page.rootInstance.componentDidLoad(); + + expect(addEventListenerSpy).toHaveBeenCalledWith( + 'click', + expect.any(Function) + ); + }); + + it('initializes popper when more than 2 actions on componentDidUpdate', async () => { + const actions = [ + { id: '1', icon: 'edit', label: 'Edit' }, + { id: '2', icon: 'delete', label: 'Delete' }, + { id: '3', icon: 'share', label: 'Share' }, + ]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + const treeActions = page.rootInstance; + const initializerSpy = jest.spyOn(treeActions as any, 'initializePopper'); + + treeActions.actions = actions; + await page.waitForChanges(); + + treeActions.componentDidUpdate(); + + expect(initializerSpy).toHaveBeenCalled(); + }); + + it('destroys popper when actions are 2 or less on componentDidUpdate', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + const treeActions = page.rootInstance; + const destroySpy = jest.fn(); + treeActions.popperInstance = { + destroy: destroySpy, + } as any; + + treeActions.actions = [{ id: '1', icon: 'edit', label: 'Edit' }]; + treeActions.componentDidUpdate(); + + expect(destroySpy).toHaveBeenCalled(); + expect(treeActions.popperInstance).toBeNull(); + }); + + it('renders with custom size', async () => { + const actions = [{ id: '1', icon: 'edit', label: 'Edit' }]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + expect(page.rootInstance.size).toBe('lg'); + }); + + it('renders disabled action', async () => { + const actions = [{ id: '1', icon: 'edit', label: 'Edit', disabled: true }]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + const button = page.root?.querySelector('modus-wc-button'); + expect(button?.hasAttribute('disabled')).toBe(true); + }); + + it('renders action with aria-label', async () => { + const actions = [ + { id: '1', icon: 'edit', label: 'Edit', ariaLabel: 'Edit item' }, + { id: '2', icon: 'delete', label: 'Delete', ariaLabel: 'Delete item' }, + ]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + const dropdownAction = page.root?.querySelector( + '.modus-wc-tree-dropdown-action' + ); + expect(dropdownAction?.getAttribute('aria-label')).toBe('Delete item'); + }); + + it('uses label as aria-label when ariaLabel is not provided', async () => { + const actions = [ + { id: '1', icon: 'edit', label: 'Edit' }, + { id: '2', icon: 'delete', label: 'Delete' }, + ]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + const dropdownAction = page.root?.querySelector( + '.modus-wc-tree-dropdown-action' + ); + expect(dropdownAction?.getAttribute('aria-label')).toBe('Delete'); + }); + + it('applies disabled class to dropdown action when disabled', async () => { + const actions = [ + { id: '1', icon: 'edit', label: 'Edit' }, + { id: '2', icon: 'delete', label: 'Delete', disabled: true }, + { id: '3', icon: 'share', label: 'Share' }, + ]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + const dropdownActions = page.root?.querySelectorAll( + '.modus-wc-tree-dropdown-action' + ); + const deleteAction = dropdownActions?.[0] as HTMLButtonElement; + + expect(deleteAction?.className).toContain('disabled'); + expect(deleteAction?.hasAttribute('disabled')).toBe(true); + }); + + it('does not apply disabled class to dropdown action when not disabled', async () => { + const actions = [ + { id: '1', icon: 'edit', label: 'Edit' }, + { id: '2', icon: 'delete', label: 'Delete' }, + { id: '3', icon: 'share', label: 'Share' }, + ]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + const dropdownActions = page.root?.querySelectorAll( + '.modus-wc-tree-dropdown-action' + ); + const deleteAction = dropdownActions?.[0] as HTMLButtonElement; + + expect(deleteAction?.className).not.toContain('disabled'); + expect(deleteAction?.hasAttribute('disabled')).toBe(false); + }); + + it('closes dropdown when action is clicked', async () => { + const actions = [ + { id: '1', icon: 'edit', label: 'Edit' }, + { id: '2', icon: 'delete', label: 'Delete' }, + ]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + const treeActions = page.rootInstance; + treeActions.isDropdownOpen = true; + + treeActions['handleActionClick']( + actions[1], + new MouseEvent('click') as any + ); + await page.waitForChanges(); + + expect(treeActions.isDropdownOpen).toBe(false); + }); + + it('initializePopper returns early when buttons are not available', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + const treeActions = page.rootInstance; + treeActions.moreActionsButton = null as any; + treeActions.moreActionsDropdown = null as any; + + treeActions['initializePopper'](); + + expect(treeActions.popperInstance).toBeNull(); + }); + + it('destroys existing popper instance before creating new one', async () => { + const actions = [ + { id: '1', icon: 'edit', label: 'Edit' }, + { id: '2', icon: 'delete', label: 'Delete' }, + { id: '3', icon: 'share', label: 'Share' }, + ]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + const treeActions = page.rootInstance; + treeActions.actions = actions; + await page.waitForChanges(); + + const mockPopper = { + destroy: jest.fn(), + }; + treeActions.popperInstance = mockPopper as any; + + treeActions.moreActionsButton = page.root?.querySelector( + '.modus-wc-tree-more-actions-wrapper modus-wc-button' + ) as HTMLElement; + treeActions.moreActionsDropdown = page.root?.querySelector( + '.modus-wc-tree-more-actions-dropdown' + ) as HTMLElement; + + treeActions['initializePopper'](); + + expect(mockPopper.destroy).toHaveBeenCalled(); + }); + + it('removes event listener and destroys popper on disconnectedCallback when popper exists', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener'); + const treeActions = page.rootInstance; + + const destroySpy = jest.fn(); + treeActions.popperInstance = { + destroy: destroySpy, + } as any; + + treeActions.disconnectedCallback(); + + expect(removeEventListenerSpy).toHaveBeenCalledWith( + 'click', + expect.any(Function) + ); + expect(destroySpy).toHaveBeenCalled(); + expect(treeActions.popperInstance).toBeNull(); + }); + + it('removes event listener on disconnectedCallback when popper is null', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener'); + const treeActions = page.rootInstance; + + treeActions.popperInstance = null; + + treeActions.disconnectedCallback(); + + expect(removeEventListenerSpy).toHaveBeenCalledWith( + 'click', + expect.any(Function) + ); + expect(treeActions.popperInstance).toBeNull(); + }); + + it('calls handleActionClick when primary action button is clicked', async () => { + const actions = [{ id: '1', icon: 'edit', label: 'Edit' }]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + const treeActions = page.rootInstance; + const handleActionClickSpy = jest.spyOn( + treeActions as any, + 'handleActionClick' + ); + + const button = page.root?.querySelector('modus-wc-button'); + button?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await page.waitForChanges(); + + expect(handleActionClickSpy).toHaveBeenCalled(); + expect(handleActionClickSpy).toHaveBeenCalledWith( + actions[0], + expect.any(MouseEvent) + ); + }); + + it('calls handleActionClick when dropdown action is clicked', async () => { + const actions = [ + { id: '1', icon: 'edit', label: 'Edit' }, + { id: '2', icon: 'delete', label: 'Delete' }, + { id: '3', icon: 'share', label: 'Share' }, + ]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + const treeActions = page.rootInstance; + treeActions.isDropdownOpen = true; + await page.waitForChanges(); + + const handleActionClickSpy = jest.spyOn( + treeActions as any, + 'handleActionClick' + ); + + const dropdownAction = page.root?.querySelector( + '.modus-wc-tree-dropdown-action' + ) as HTMLButtonElement; + dropdownAction?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await page.waitForChanges(); + + expect(handleActionClickSpy).toHaveBeenCalled(); + expect(handleActionClickSpy).toHaveBeenCalledWith( + actions[1], + expect.any(MouseEvent) + ); + }); }); diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts index 882de23eec..3d87c6cc02 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts @@ -1,5 +1,5 @@ import { newSpecPage } from '@stencil/core/testing'; -import { ModusWcTreeItem } from './modus-wc-tree-item'; +import { ModusWcTreeItem, ModusWcTreeItemElement } from './modus-wc-tree-item'; describe('modus-wc-tree-item', () => { it('renders with default props', async () => { @@ -17,4 +17,817 @@ describe('modus-wc-tree-item', () => { }); expect(page.root).toMatchSnapshot(); }); + + it('renders with disabled state', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const li = page.root?.querySelector('li'); + expect(li?.tabIndex).toBe(-1); + }); + + it('renders with selected state', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const li = page.root?.querySelector('li'); + expect(li?.getAttribute('aria-selected')).toBe('true'); + }); + + it('renders with custom class', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const li = page.root?.querySelector('li'); + expect(li?.classList.contains('custom-item')).toBe(true); + }); + + it('renders with subtree', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const li = page.root?.querySelector('li'); + expect(li?.getAttribute('aria-expanded')).toBe('false'); + }); + + it('emits itemSelect event on click when no checkbox', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const itemSelectSpy = jest.fn(); + page.root?.addEventListener('itemSelect', itemSelectSpy); + + const li = page.root?.querySelector('li'); + li?.click(); + + await page.waitForChanges(); + + expect(itemSelectSpy).toHaveBeenCalled(); + expect(itemSelectSpy.mock.calls[0][0].detail.value).toBe('test-value'); + }); + + it('does not emit itemSelect when checkbox is enabled', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const itemSelectSpy = jest.fn(); + page.root?.addEventListener('itemSelect', itemSelectSpy); + + const li = page.root?.querySelector('li'); + li?.click(); + + await page.waitForChanges(); + + expect(itemSelectSpy).not.toHaveBeenCalled(); + }); + + it('handles Enter key to emit itemSelect', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const itemSelectSpy = jest.fn(); + page.root?.addEventListener('itemSelect', itemSelectSpy); + + const li = page.root?.querySelector('li'); + li?.dispatchEvent( + new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }) + ); + + await page.waitForChanges(); + + expect(itemSelectSpy).toHaveBeenCalled(); + }); + + it('handles Space key to emit itemSelect', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const itemSelectSpy = jest.fn(); + page.root?.addEventListener('itemSelect', itemSelectSpy); + + const li = page.root?.querySelector('li'); + li?.dispatchEvent( + new KeyboardEvent('keydown', { key: ' ', bubbles: true }) + ); + + await page.waitForChanges(); + + expect(itemSelectSpy).toHaveBeenCalled(); + }); + + it('toggles subtree on toggle icon click', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ` + +
      Child content
      +
      + `, + }); + + const treeItem = page.rootInstance; + const submenu = page.root?.querySelector( + '.modus-wc-tree-dropdown' + ) as HTMLElement; + + expect(submenu.classList.contains('modus-wc-tree-dropdown-show')).toBe( + false + ); + + // Manually call the toggle method + const event = new MouseEvent('click', { bubbles: true }); + treeItem['handleToggleClick'](event); + await page.waitForChanges(); + + expect(submenu.classList.contains('modus-wc-tree-dropdown-show')).toBe( + true + ); + + treeItem['handleToggleClick'](event); + await page.waitForChanges(); + + expect(submenu.classList.contains('modus-wc-tree-dropdown-show')).toBe( + false + ); + }); + + it('expandSubTree method expands the subtree', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ` + +
      Child content
      +
      + `, + }); + + const treeItem = page.rootInstance; + const submenu = page.root?.querySelector( + '.modus-wc-tree-dropdown' + ) as HTMLElement; + + expect(submenu.classList.contains('modus-wc-tree-dropdown-show')).toBe( + false + ); + + await treeItem.expandSubTree(); + await page.waitForChanges(); + + expect(submenu.classList.contains('modus-wc-tree-dropdown-show')).toBe( + true + ); + }); + + it('collapseSubTree method collapses the subtree', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ` + +
      Child content
      +
      + `, + }); + + const treeItem = page.rootInstance; + treeItem.isExpanded = true; + const submenu = page.root?.querySelector( + '.modus-wc-tree-dropdown' + ) as HTMLElement; + + expect(submenu.classList.contains('modus-wc-tree-dropdown-show')).toBe( + true + ); + + await treeItem.collapseSubTree(); + await page.waitForChanges(); + + expect(submenu.classList.contains('modus-wc-tree-dropdown-show')).toBe( + false + ); + }); + + it('expandSubTree does nothing when already expanded', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ` + +
      Child content
      +
      + `, + }); + + const treeItem = page.rootInstance; + treeItem.isExpanded = true; + + const result = await treeItem.expandSubTree(); + expect(result).toBeUndefined(); + }); + + it('collapseSubTree does nothing when already collapsed', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ` + +
      Child content
      +
      + `, + }); + + const treeItem = page.rootInstance; + const result = await treeItem.collapseSubTree(); + expect(result).toBeUndefined(); + }); + + it('expandSubTree does nothing when no subtree', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const treeItem = page.rootInstance; + const result = await treeItem.expandSubTree(); + expect(result).toBeUndefined(); + }); + + it('collapseSubTree does nothing when no subtree', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const treeItem = page.rootInstance; + const result = await treeItem.collapseSubTree(); + expect(result).toBeUndefined(); + }); + + it('checkbox click toggles selection', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const treeItem = page.rootInstance; + expect(treeItem.selected).toBeFalsy(); + + const checkbox = page.root?.querySelector('modus-wc-checkbox'); + checkbox?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await page.waitForChanges(); + + expect(treeItem.selected).toBe(true); + }); + + it('checkbox handles Enter key', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const treeItem = page.rootInstance; + const checkbox = page.root?.querySelector('modus-wc-checkbox'); + + checkbox?.dispatchEvent( + new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }) + ); + await page.waitForChanges(); + + expect(treeItem.selected).toBe(true); + }); + + it('checkbox handles Space key', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const treeItem = page.rootInstance; + const checkbox = page.root?.querySelector('modus-wc-checkbox'); + + checkbox?.dispatchEvent( + new KeyboardEvent('keydown', { key: ' ', bubbles: true }) + ); + await page.waitForChanges(); + + expect(treeItem.selected).toBe(true); + }); + + it('updates children selection when parent checkbox is clicked', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ` + +
      + + +
      +
      + `, + }); + + const parent = page.rootInstance; + const children = page.root?.querySelectorAll( + '.modus-wc-tree-dropdown modus-wc-tree-item' + ) as NodeListOf; + + const checkbox = page.root?.querySelector('modus-wc-checkbox'); + checkbox?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await page.waitForChanges(); + + expect(parent.selected).toBe(true); + expect(children[0].selected).toBe(true); + expect(children[1].selected).toBe(true); + }); + + it('sets indeterminate state when some children are selected', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ` + +
      + + +
      +
      + `, + }); + + const parent = page.rootInstance; + + // Manually trigger the update + const event = new CustomEvent('selectionsChange', { bubbles: true }); + Object.defineProperty(event, 'target', { + value: page.root?.querySelector('modus-wc-tree-item[value="child1"]'), + enumerable: true, + }); + + parent['updateIndeterminateState'](event); + await page.waitForChanges(); + + expect(parent.isIndeterminate).toBe(true); + expect(parent.selected).toBe(false); + }); + + it('sets selected state when all children are selected', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ` + +
      + + +
      +
      + `, + }); + + const parent = page.rootInstance; + + const event = new CustomEvent('selectionsChange', { bubbles: true }); + Object.defineProperty(event, 'target', { + value: page.root?.querySelector('modus-wc-tree-item[value="child1"]'), + enumerable: true, + }); + + parent['updateIndeterminateState'](event); + await page.waitForChanges(); + + expect(parent.isIndeterminate).toBe(false); + expect(parent.selected).toBe(true); + }); + + it('updateIndeterminateState returns early when event target is self', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const parent = page.rootInstance; + const event = new CustomEvent('selectionsChange', { bubbles: true }); + Object.defineProperty(event, 'target', { + value: page.root, + enumerable: true, + }); + + parent['updateIndeterminateState'](event); + + expect(parent.isIndeterminate).toBe(false); + }); + + it('updateIndeterminateState returns early when no subtree', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const item = page.rootInstance; + const event = new CustomEvent('selectionsChange', { bubbles: true }); + Object.defineProperty(event, 'target', { + value: null, + enumerable: true, + }); + + item['updateIndeterminateState'](event); + + expect(item.isIndeterminate).toBe(false); + }); + + it('updateIndeterminateState returns early when no checkbox', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const parent = page.rootInstance; + const event = new CustomEvent('selectionsChange', { bubbles: true }); + + parent['updateIndeterminateState'](event); + + expect(parent.isIndeterminate).toBe(false); + }); + + it('updateIndeterminateState returns early when submenu is not found', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const parent = page.rootInstance; + const event = new CustomEvent('selectionsChange', { bubbles: true }); + Object.defineProperty(event, 'target', { + value: null, + enumerable: true, + }); + + parent['updateIndeterminateState'](event); + + expect(parent.isIndeterminate).toBe(false); + }); + + it('updateChildrenSelection returns early when no subtree', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const item = page.rootInstance; + item['updateChildrenSelection'](true); + + // Should return early without error + expect(item.hasSubtree).toBeFalsy(); + }); + + it('updateChildrenSelection returns early when submenu is not found', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const parent = page.rootInstance; + parent['updateChildrenSelection'](true); + + // Should return early without error + expect(parent.hasSubtree).toBe(true); + }); + + it('updateChildrenSelection skips items without checkbox', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ` + + + + + + + `, + }); + + const parent = page.rootInstance; + parent['updateChildrenSelection'](true); + await page.waitForChanges(); + + const submenu = page.root?.querySelector('.modus-wc-tree-dropdown'); + const children = Array.from( + submenu?.querySelectorAll('modus-wc-tree-item') || [] + ) as ModusWcTreeItemElement[]; + + // First child with checkbox should be selected + expect(children[0]?.selected).toBe(true); + // Second child without checkbox should remain unchanged (not selected) + expect(children[1]?.selected).toBeFalsy(); + }); + + it('adds event listener on componentDidLoad when has subtree', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const addEventListenerSpy = jest.spyOn( + page.root as HTMLElement, + 'addEventListener' + ); + + page.rootInstance.componentDidLoad(); + + expect(addEventListenerSpy).toHaveBeenCalledWith( + 'selectionsChange', + expect.any(Function) + ); + }); + + it('removes event listener on disconnectedCallback', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const removeEventListenerSpy = jest.spyOn( + page.root as HTMLElement, + 'removeEventListener' + ); + + page.rootInstance.disconnectedCallback(); + + expect(removeEventListenerSpy).toHaveBeenCalledWith( + 'selectionsChange', + expect.any(Function) + ); + }); + + it('renders tree item actions when provided', async () => { + const actions = [ + { icon: 'edit', label: 'Edit' }, + { icon: 'delete', label: 'Delete' }, + ]; + + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + page.rootInstance.treeItemActions = actions; + await page.waitForChanges(); + + const actionsElement = page.root?.querySelector('modus-wc-tree-actions'); + expect(actionsElement).toBeDefined(); + }); + + it('renders with different sizes', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + expect(page.rootInstance.size).toBe('md'); + }); + + it('renders with start icon slot', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ` + + + + `, + }); + + const slot = page.root?.querySelector('slot[name="start-icon"]'); + expect(slot).toBeDefined(); + }); + + it('inherits ARIA attributes', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const li = page.root?.querySelector('li'); + expect(li?.getAttribute('aria-label')).toBe('Custom Label'); + }); + + it('getClasses includes disabled class when disabled', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const li = page.root?.querySelector('li'); + expect(li?.className).toContain('modus-wc-tree-item'); + }); + + it('getClasses includes selected class when selected', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const li = page.root?.querySelector('li'); + expect(li?.className).toContain('modus-wc-tree-item'); + }); + + it('toggle icon shows chevron_right when collapsed', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const treeItem = page.rootInstance; + expect(treeItem.isExpanded).toBe(false); + + // Check the icon in the rendered output + const icon = page.root?.querySelector('modus-wc-icon'); + expect(icon).toBeDefined(); + }); + + it('toggle icon shows expand_more when expanded', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const treeItem = page.rootInstance; + treeItem.isExpanded = true; + await page.waitForChanges(); + + expect(treeItem.isExpanded).toBe(true); + + // Check the icon in the rendered output + const icon = page.root?.querySelector('modus-wc-icon'); + expect(icon).toBeDefined(); + }); + + it('handleToggleClick does nothing when no subtree', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const treeItem = page.rootInstance; + const initialExpanded = treeItem.isExpanded; + + const event = new MouseEvent('click', { bubbles: true }); + treeItem['handleToggleClick'](event); + + expect(treeItem.isExpanded).toBe(initialExpanded); + }); + + it('checkbox from indeterminate to selected on click', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const treeItem = page.rootInstance; + treeItem.isIndeterminate = true; + + const checkbox = page.root?.querySelector('modus-wc-checkbox'); + checkbox?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await page.waitForChanges(); + + expect(treeItem.selected).toBe(true); + expect(treeItem.isIndeterminate).toBe(false); + }); + + it('emits selectionsChange with selected values when checkbox is clicked in tree', async () => { + const contentTree = document.createElement('modus-wc-content-tree'); + const treeView = document.createElement('modus-wc-tree-view'); + contentTree.appendChild(treeView); + + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + treeView.appendChild(page.root as HTMLElement); + document.body.appendChild(contentTree); + + const eventSpy = jest.fn(); + page.root?.addEventListener('selectionsChange', eventSpy); + + const treeItem = page.rootInstance; + treeItem['handleCheckboxClick'](new MouseEvent('click') as any); + await page.waitForChanges(); + + expect(eventSpy).toHaveBeenCalled(); + expect(eventSpy.mock.calls[0][0].detail.selectedValues).toEqual(['item1']); + + document.body.removeChild(contentTree); + }); + + it('does not emit selectionsChange when rootTreeView is not found', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const eventSpy = jest.fn(); + page.root?.addEventListener('selectionsChange', eventSpy); + + const treeItem = page.rootInstance; + treeItem['handleCheckboxClick'](new MouseEvent('click') as any); + await page.waitForChanges(); + + expect(eventSpy).not.toHaveBeenCalled(); + }); + + it('handleCheckboxClick sets newValue to true when selected is false', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const treeItem = page.rootInstance; + treeItem.selected = false; + treeItem.isIndeterminate = false; + + treeItem['handleCheckboxClick'](); + await page.waitForChanges(); + + expect(treeItem.selected).toBe(true); + expect(treeItem.isIndeterminate).toBe(false); + }); + + it('handleCheckboxClick sets newValue to false when selected is true and not indeterminate', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const treeItem = page.rootInstance; + treeItem.isIndeterminate = false; + + treeItem['handleCheckboxClick'](); + await page.waitForChanges(); + + expect(treeItem.selected).toBe(false); + expect(treeItem.isIndeterminate).toBe(false); + }); + + it('handleCheckboxClick sets newValue to true when isIndeterminate is true', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const treeItem = page.rootInstance; + treeItem.selected = false; + treeItem.isIndeterminate = true; + + treeItem['handleCheckboxClick'](); + await page.waitForChanges(); + + expect(treeItem.selected).toBe(true); + expect(treeItem.isIndeterminate).toBe(false); + }); + + it('handleCheckboxClick sets newValue to true when selected is true but isIndeterminate is also true', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const treeItem = page.rootInstance; + treeItem.isIndeterminate = true; + + treeItem['handleCheckboxClick'](); + await page.waitForChanges(); + + // When indeterminate is true, newValue should be true (OR condition) + expect(treeItem.selected).toBe(true); + expect(treeItem.isIndeterminate).toBe(false); + }); + + it('handleCheckboxClick resets isIndeterminate to false after click', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const treeItem = page.rootInstance; + treeItem.selected = false; + treeItem.isIndeterminate = true; + + expect(treeItem.isIndeterminate).toBe(true); + + treeItem['handleCheckboxClick'](); + await page.waitForChanges(); + + expect(treeItem.isIndeterminate).toBe(false); + }); }); diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx index e871c4fb3e..6015fcc9aa 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx @@ -14,17 +14,14 @@ import { ModusSize } from '../../types'; import { Attributes, inheritAriaAttributes } from '../../utils'; import { ModusTreeItemActions } from '../modus-wc-tree-actions/modus-wc-tree-actions'; -export interface IMenuItemElement extends HTMLElement { - /** The unique identifying value of the tree item. */ +export interface ModusWcTreeItemElement extends HTMLElement { value: string; - /** The selected state of the tree item. */ selected?: boolean; - /** Whether the item has a checkbox (used for selection in tree structure) */ checkbox?: boolean; - /** Whether this item has a submenu (used for tree structure) */ - hasSubmenu?: boolean; - /** Whether the checkbox is in an indeterminate state (only applicable if checkbox is true) */ + hasSubtree?: boolean; isIndeterminate?: boolean; + collapseSubTree(): Promise; + expandSubTree(): Promise; } /** @@ -166,7 +163,9 @@ export class ModusWcTreeItem { '.modus-wc-tree-dropdown' ) as HTMLElement; - submenu?.classList.toggle('modus-wc-tree-dropdown-show'); + if (submenu) { + submenu.classList.toggle('modus-wc-tree-dropdown-show'); + } }; private handleKeyDown = (e: KeyboardEvent) => { @@ -199,8 +198,9 @@ export class ModusWcTreeItem { const childMenuItems = Array.from(submenu.children).filter( (el) => - el.tagName === 'MODUS-WC-TREE-ITEM' && (el as IMenuItemElement).checkbox - ) as IMenuItemElement[]; + el.tagName === 'MODUS-WC-TREE-ITEM' && + (el as ModusWcTreeItemElement).checkbox + ) as ModusWcTreeItemElement[]; let selectedCount = 0; @@ -222,7 +222,7 @@ export class ModusWcTreeItem { const descendants = Array.from( submenu.querySelectorAll('modus-wc-tree-item') - ) as IMenuItemElement[]; + ) as ModusWcTreeItemElement[]; descendants.forEach((item) => { if (!item.checkbox) return; @@ -257,7 +257,7 @@ export class ModusWcTreeItem { if (rootTreeView) { const allTreeItems = Array.from( rootTreeView.querySelectorAll('modus-wc-tree-item') - ) as IMenuItemElement[]; + ) as ModusWcTreeItemElement[]; const selectedValues = allTreeItems .filter((item) => item.checkbox && item.selected) .map((item) => item.value); diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/__snapshots__/modus-wc-tree-view.spec.ts.snap b/src/components/modus-wc-content-tree/modus-wc-tree-view/__snapshots__/modus-wc-tree-view.spec.ts.snap index 4c1a52e73b..12e5eb1cbc 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-view/__snapshots__/modus-wc-tree-view.spec.ts.snap +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/__snapshots__/modus-wc-tree-view.spec.ts.snap @@ -1,10 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`modus-wc-tree-view renders as a sublist when isSubList is true 1`] = ` + + +
        +
        +`; + exports[`modus-wc-tree-view renders with custom props 1`] = ` - + -
          +
          `; diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts index 1f9305a165..debf67bee1 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts @@ -22,4 +22,230 @@ describe('modus-wc-tree-view', () => { }); expect(page.root).toMatchSnapshot(); }); + + it('renders as a sublist when isSubList is true', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeView], + html: ``, + }); + + expect(page.root).toMatchSnapshot(); + + const host = page.root as HTMLElement; + expect(host.classList.contains('modus-wc-tree-submenu')).toBe(true); + + const ul = host.querySelector('ul'); + expect(ul?.classList.contains('modus-wc-tree-dropdown')).toBe(true); + expect(ul?.getAttribute('role')).toBe('group'); + }); + + it('renders as a tree when isSubList is false', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeView], + html: ``, + }); + + const ul = page.root?.querySelector('ul'); + expect(ul?.classList.contains('modus-wc-menu')).toBe(true); + expect(ul?.classList.contains('modus-wc-tree-view')).toBe(true); + expect(ul?.getAttribute('role')).toBe('tree'); + }); + + it('applies custom class to sublist', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeView], + html: ``, + }); + + const ul = page.root?.querySelector('ul'); + expect(ul?.classList.contains('modus-wc-tree-dropdown')).toBe(true); + expect(ul?.classList.contains('custom-sublist')).toBe(true); + }); + + it('applies custom class to main tree view', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeView], + html: ``, + }); + + const ul = page.root?.querySelector('ul'); + expect(ul?.classList.contains('modus-wc-menu')).toBe(true); + expect(ul?.classList.contains('modus-wc-tree-view')).toBe(true); + expect(ul?.classList.contains('custom-tree')).toBe(true); + }); + + it('handles itemSelect event and marks item as active', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeView], + html: ` + +
          +
          Item 1
          +
          +
          +
          Item 2
          +
          +
          + `, + }); + + const treeView = page.rootInstance; + const items = page.root?.querySelectorAll('.modus-wc-tree-item'); + const firstItem = items?.[0] as HTMLElement; + const secondItem = items?.[1] as HTMLElement; + + // Simulate item selection on first item + const event = new CustomEvent('itemSelect', { + detail: { value: 'item1' }, + bubbles: true, + }); + Object.defineProperty(event, 'target', { + value: firstItem, + enumerable: true, + }); + + treeView.handleItemSelect(event); + await page.waitForChanges(); + + const firstContent = firstItem.querySelector('.modus-wc-tree-content'); + expect(firstContent?.classList.contains('modus-wc-tree-item-active')).toBe( + true + ); + + // Simulate item selection on second item + const event2 = new CustomEvent('itemSelect', { + detail: { value: 'item2' }, + bubbles: true, + }); + Object.defineProperty(event2, 'target', { + value: secondItem, + enumerable: true, + }); + + treeView.handleItemSelect(event2); + await page.waitForChanges(); + + // First item should no longer be active + expect(firstContent?.classList.contains('modus-wc-tree-item-active')).toBe( + false + ); + + // Second item should be active + const secondContent = secondItem.querySelector('.modus-wc-tree-content'); + expect(secondContent?.classList.contains('modus-wc-tree-item-active')).toBe( + true + ); + }); + + it('handles itemSelect event when target is missing', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeView], + html: ``, + }); + + const treeView = page.rootInstance; + + // Simulate event with no target + const event = new CustomEvent('itemSelect', { + detail: { value: 'item1' }, + bubbles: true, + }); + Object.defineProperty(event, 'target', { + value: null, + enumerable: true, + }); + + // Should not throw + expect(() => treeView.handleItemSelect(event)).not.toThrow(); + }); + + it('handles itemSelect event when target has no content element', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeView], + html: ` + +
          No content here
          +
          + `, + }); + + const treeView = page.rootInstance; + const item = page.root?.querySelector('.modus-wc-tree-item') as HTMLElement; + + const event = new CustomEvent('itemSelect', { + detail: { value: 'item1' }, + bubbles: true, + }); + Object.defineProperty(event, 'target', { + value: item, + enumerable: true, + }); + + // Should not throw even when content element is missing + expect(() => treeView.handleItemSelect(event)).not.toThrow(); + }); + + it('inherits ARIA attributes', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeView], + html: ``, + }); + + const ul = page.root?.querySelector('ul'); + expect(ul?.getAttribute('aria-label')).toBe('Test Tree'); + expect(ul?.getAttribute('aria-expanded')).toBe('true'); + }); + + it('getClasses returns correct classes for main tree view', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeView], + html: ``, + }); + + const treeView = page.rootInstance; + const classes = treeView['getClasses'](); + + expect(classes).toContain('modus-wc-menu'); + expect(classes).toContain('modus-wc-tree-view'); + }); + + it('getClasses returns correct classes for sublist', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeView], + html: ``, + }); + + const treeView = page.rootInstance; + const classes = treeView['getClasses'](); + + expect(classes).toContain('modus-wc-tree-dropdown'); + expect(classes).not.toContain('modus-wc-menu'); + }); + + it('getClasses includes custom class for main tree view', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeView], + html: ``, + }); + + const treeView = page.rootInstance; + const classes = treeView['getClasses'](); + + expect(classes).toContain('modus-wc-menu'); + expect(classes).toContain('modus-wc-tree-view'); + expect(classes).toContain('my-custom-class'); + }); + + it('getClasses includes custom class for sublist', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeView], + html: ``, + }); + + const treeView = page.rootInstance; + const classes = treeView['getClasses'](); + + expect(classes).toContain('modus-wc-tree-dropdown'); + expect(classes).toContain('my-custom-sublist'); + }); }); diff --git a/src/components/modus-wc-content-tree/readme.md b/src/components/modus-wc-content-tree/readme.md index 4f60bb3fd9..9340e61db5 100644 --- a/src/components/modus-wc-content-tree/readme.md +++ b/src/components/modus-wc-content-tree/readme.md @@ -8,7 +8,6 @@ ## Overview A customizable content tree component used to display hierarchical data in a tree structure. -Uses menu items to create the tree structure with support for expanding/collapsing nodes and selection. ## Properties diff --git a/src/custom-elements.json b/src/custom-elements.json index 6bacb0eb41..fe94bcdc6e 100644 --- a/src/custom-elements.json +++ b/src/custom-elements.json @@ -2293,7 +2293,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable content tree component used to display hierarchical data in a tree structure.\r\nUses menu items to create the tree structure with support for expanding/collapsing nodes and selection.", + "description": "A customizable content tree component used to display hierarchical data in a tree structure.", "name": "ModusWcContentTree", "members": [ { From fa4b5c0840bdce26b56e9f44cdb14ed8471b02a5 Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:37:58 +0530 Subject: [PATCH 19/39] 662 - build fixes --- src/components.d.ts | 20 +++++-- .../modus-wc-autocomplete/readme.md | 5 +- src/components/modus-wc-button/readme.md | 4 ++ src/components/modus-wc-card/readme.md | 2 +- src/components/modus-wc-checkbox/readme.md | 2 + .../modus-wc-content-tree.spec.ts | 55 ++++++++++--------- .../modus-wc-content-tree.tsx | 14 ++--- .../modus-wc-tree-actions.spec.ts | 41 ++++++-------- .../modus-wc-tree-item.spec.ts | 4 +- src/custom-elements.json | 2 +- 10 files changed, 84 insertions(+), 65 deletions(-) diff --git a/src/components.d.ts b/src/components.d.ts index 1164e36207..504a22237f 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -2095,9 +2095,12 @@ export namespace Components { } /** * A customizable typography component used to render text with different sizes, hierarchy, and weights. - * Note: When using heading elements (h1-h6), the default heading CSS styling can be accessed without modifying + * Note: + * - When using heading elements (h1-h6), the default heading CSS styling can be accessed without modifying * the default size (size="md") and weight (weight="normal") properties. Default styling can be overridden by * providing your own custom values for the size or weight properties from the available options. + * - If both slot content and `label` are provided, only the slot content will be rendered + * - Use the `label` prop when you need to dynamically update the text. */ interface ModusWcTypography { /** @@ -3203,9 +3206,12 @@ declare global { }; /** * A customizable typography component used to render text with different sizes, hierarchy, and weights. - * Note: When using heading elements (h1-h6), the default heading CSS styling can be accessed without modifying + * Note: + * - When using heading elements (h1-h6), the default heading CSS styling can be accessed without modifying * the default size (size="md") and weight (weight="normal") properties. Default styling can be overridden by * providing your own custom values for the size or weight properties from the available options. + * - If both slot content and `label` are provided, only the slot content will be rendered + * - Use the `label` prop when you need to dynamically update the text. */ interface HTMLModusWcTypographyElement extends Components.ModusWcTypography, HTMLStencilElement { } @@ -5672,9 +5678,12 @@ declare namespace LocalJSX { } /** * A customizable typography component used to render text with different sizes, hierarchy, and weights. - * Note: When using heading elements (h1-h6), the default heading CSS styling can be accessed without modifying + * Note: + * - When using heading elements (h1-h6), the default heading CSS styling can be accessed without modifying * the default size (size="md") and weight (weight="normal") properties. Default styling can be overridden by * providing your own custom values for the size or weight properties from the available options. + * - If both slot content and `label` are provided, only the slot content will be rendered + * - Use the `label` prop when you need to dynamically update the text. */ interface ModusWcTypography { /** @@ -6015,9 +6024,12 @@ declare module "@stencil/core" { "modus-wc-tree-view": LocalJSX.ModusWcTreeView & JSXBase.HTMLAttributes; /** * A customizable typography component used to render text with different sizes, hierarchy, and weights. - * Note: When using heading elements (h1-h6), the default heading CSS styling can be accessed without modifying + * Note: + * - When using heading elements (h1-h6), the default heading CSS styling can be accessed without modifying * the default size (size="md") and weight (weight="normal") properties. Default styling can be overridden by * providing your own custom values for the size or weight properties from the available options. + * - If both slot content and `label` are provided, only the slot content will be rendered + * - Use the `label` prop when you need to dynamically update the text. */ "modus-wc-typography": LocalJSX.ModusWcTypography & JSXBase.HTMLAttributes; "modus-wc-utility-panel": LocalJSX.ModusWcUtilityPanel & JSXBase.HTMLAttributes; diff --git a/src/components/modus-wc-autocomplete/readme.md b/src/components/modus-wc-autocomplete/readme.md index db7e9e2206..a41d95aacd 100644 --- a/src/components/modus-wc-autocomplete/readme.md +++ b/src/components/modus-wc-autocomplete/readme.md @@ -23,6 +23,7 @@ The component supports a `` for injecting custom content. | `customKeyDown` | `custom-key-down` | Custom key down handler - if provided, overrides default keyboard navigation | `((event: KeyboardEvent) => void) \| undefined` | `undefined` | | `debounceMs` | `debounce-ms` | The debounce timeout in milliseconds. Set to 0 to disable debouncing. | `number \| undefined` | `300` | | `disabled` | `disabled` | Whether the form control is disabled. | `boolean \| undefined` | `false` | +| `feedback` | `feedback` | Feedback state for the input field. | `IInputFeedbackProp \| undefined` | `undefined` | | `includeClear` | `include-clear` | Show the clear button within the input field. | `boolean \| undefined` | `false` | | `includeSearch` | `include-search` | Show the search icon within the input field. | `boolean \| undefined` | `false` | | `inputId` | `input-id` | The ID of the input element. | `string \| undefined` | `undefined` | @@ -132,6 +133,7 @@ Type: `Promise` ### Depends on - [modus-wc-input-label](../modus-wc-input-label) +- [modus-wc-input-feedback](../modus-wc-input-feedback) - [modus-wc-menu](../modus-wc-menu) - [modus-wc-chip](../modus-wc-chip) - [modus-wc-button](../modus-wc-button) @@ -144,6 +146,7 @@ Type: `Promise` ```mermaid graph TD; modus-wc-autocomplete --> modus-wc-input-label + modus-wc-autocomplete --> modus-wc-input-feedback modus-wc-autocomplete --> modus-wc-menu modus-wc-autocomplete --> modus-wc-chip modus-wc-autocomplete --> modus-wc-button @@ -151,9 +154,9 @@ graph TD; modus-wc-autocomplete --> modus-wc-text-input modus-wc-autocomplete --> modus-wc-loader modus-wc-autocomplete --> modus-wc-menu-item + modus-wc-input-feedback --> modus-wc-icon modus-wc-text-input --> modus-wc-input-label modus-wc-text-input --> modus-wc-input-feedback - modus-wc-input-feedback --> modus-wc-icon modus-wc-menu-item --> modus-wc-checkbox modus-wc-menu-item --> modus-wc-tooltip modus-wc-checkbox --> modus-wc-input-label diff --git a/src/components/modus-wc-button/readme.md b/src/components/modus-wc-button/readme.md index beb39aef4b..8e5a0dc1f1 100644 --- a/src/components/modus-wc-button/readme.md +++ b/src/components/modus-wc-button/readme.md @@ -40,8 +40,10 @@ The component supports a `` for injecting content within the button, simil - [modus-wc-content-tree](../modus-wc-content-tree) - [modus-wc-date](../modus-wc-date) - [modus-wc-dropdown-menu](../modus-wc-dropdown-menu) + - [modus-wc-handle](../modus-wc-handle) - [modus-wc-modal](../modus-wc-modal) - [modus-wc-navbar](../modus-wc-navbar) + - [modus-wc-tree-actions](../modus-wc-content-tree/modus-wc-tree-actions) ### Graph ```mermaid @@ -51,8 +53,10 @@ graph TD; modus-wc-content-tree --> modus-wc-button modus-wc-date --> modus-wc-button modus-wc-dropdown-menu --> modus-wc-button + modus-wc-handle --> modus-wc-button modus-wc-modal --> modus-wc-button modus-wc-navbar --> modus-wc-button + modus-wc-tree-actions --> modus-wc-button style modus-wc-button fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/src/components/modus-wc-card/readme.md b/src/components/modus-wc-card/readme.md index 9961a018a9..cde71ab859 100644 --- a/src/components/modus-wc-card/readme.md +++ b/src/components/modus-wc-card/readme.md @@ -9,7 +9,7 @@ A customizable card component used to group and display content in a way that is easily readable. -The component supports six `` called 'header' (for images/custom content), 'title', 'subtitle', default slot (main body), 'actions' (buttons/interactive elements), and 'footer'. +This component supports multiple `` elements including 'header' for images or custom content, 'title', 'subtitle', a default slot for main content, 'actions' for buttons or interactive elements, and 'footer'. ## Properties diff --git a/src/components/modus-wc-checkbox/readme.md b/src/components/modus-wc-checkbox/readme.md index 88c20f3235..f7a588f84f 100644 --- a/src/components/modus-wc-checkbox/readme.md +++ b/src/components/modus-wc-checkbox/readme.md @@ -40,6 +40,7 @@ A customizable checkbox component - [modus-wc-menu-item](../modus-wc-menu-item) - [modus-wc-table](../modus-wc-table) + - [modus-wc-tree-item](../modus-wc-content-tree/modus-wc-tree-item) ### Depends on @@ -51,6 +52,7 @@ graph TD; modus-wc-checkbox --> modus-wc-input-label modus-wc-menu-item --> modus-wc-checkbox modus-wc-table --> modus-wc-checkbox + modus-wc-tree-item --> modus-wc-checkbox style modus-wc-checkbox fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts index 0817c31457..d4d165a767 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts @@ -102,7 +102,8 @@ describe('modus-wc-content-tree', () => { )[1] as ModusWcTreeItemElement; // Mock expandSubTree on parent - parent.expandSubTree = jest.fn().mockResolvedValue(undefined); + const expandSubTreeMock = jest.fn().mockResolvedValue(undefined); + parent.expandSubTree = expandSubTreeMock; const tree = page.rootInstance; @@ -117,7 +118,7 @@ describe('modus-wc-content-tree', () => { expect(parent.style.display).toBe(''); // Parent expansion should be triggered - expect(parent.expandSubTree).toHaveBeenCalled(); + expect(expandSubTreeMock).toHaveBeenCalled(); }); it('updateSlotContent returns early if slotEl is not set', async () => { @@ -152,11 +153,12 @@ describe('modus-wc-content-tree', () => { ) as ModusWcTreeItemElement; item.hasSubtree = true; - item.expandSubTree = jest.fn().mockResolvedValue(undefined); + const expandMock = jest.fn().mockResolvedValue(undefined); + item.expandSubTree = expandMock; await tree.toggleExpandCollapse(); - expect(item.expandSubTree).toHaveBeenCalled(); + expect(expandMock).toHaveBeenCalled(); }); it('skips nodes without subtrees', async () => { @@ -481,14 +483,13 @@ describe('modus-wc-content-tree', () => { ) as ModusWcTreeItemElement; // Mock expandSubTree to reject with error - parent.expandSubTree = jest - .fn() - .mockRejectedValue(new Error('Expand failed')); + const expandMock = jest.fn().mockRejectedValue(new Error('Expand failed')); + parent.expandSubTree = expandMock; // Should not throw despite the error await expect(tree.filterNodes('Child')).resolves.not.toThrow(); - expect(parent.expandSubTree).toHaveBeenCalled(); + expect(expandMock).toHaveBeenCalled(); }); it('filterNodes continues expanding other items when one fails', async () => { @@ -512,16 +513,18 @@ describe('modus-wc-content-tree', () => { ) as NodeListOf; // First parent fails - parents[0].expandSubTree = jest.fn().mockRejectedValue(new Error('Failed')); + const expandMock1 = jest.fn().mockRejectedValue(new Error('Failed')); + parents[0].expandSubTree = expandMock1; // Second parent succeeds - parents[1].expandSubTree = jest.fn().mockResolvedValue(undefined); + const expandMock2 = jest.fn().mockResolvedValue(undefined); + parents[1].expandSubTree = expandMock2; await tree.filterNodes('Match'); // Both should be called despite first one failing - expect(parents[0].expandSubTree).toHaveBeenCalled(); - expect(parents[1].expandSubTree).toHaveBeenCalled(); + expect(expandMock1).toHaveBeenCalled(); + expect(expandMock2).toHaveBeenCalled(); }); it('updateSlotContent invalidates cached items', async () => { @@ -531,7 +534,7 @@ describe('modus-wc-content-tree', () => { }); const tree = page.rootInstance; - tree['cachedItems'] = [] as any; + tree['cachedItems'] = []; const mockSlot = { assignedNodes: jest.fn().mockReturnValue([]), @@ -579,11 +582,12 @@ describe('modus-wc-content-tree', () => { const item = page.root?.querySelector( 'modus-wc-tree-item' ) as ModusWcTreeItemElement; - item.collapseSubTree = jest.fn().mockResolvedValue(undefined); + const collapseMock = jest.fn().mockResolvedValue(undefined); + item.collapseSubTree = collapseMock; await tree.toggleExpandCollapse(); - expect(item.collapseSubTree).toHaveBeenCalled(); + expect(collapseMock).toHaveBeenCalled(); expect(tree.areAllExpanded).toBe(false); }); @@ -642,12 +646,13 @@ describe('modus-wc-content-tree', () => { const parent = page.root?.querySelector( 'modus-wc-tree-item' ) as ModusWcTreeItemElement; - parent.expandSubTree = jest.fn().mockResolvedValue(undefined); + const expandMock = jest.fn().mockResolvedValue(undefined); + parent.expandSubTree = expandMock; await tree.filterNodes('Child'); - expect((parent as HTMLElement).style.display).toBe(''); - expect(parent.expandSubTree).toHaveBeenCalled(); + expect(parent.style.display).toBe(''); + expect(expandMock).toHaveBeenCalled(); }); it('inherits ARIA attributes', async () => { @@ -685,15 +690,15 @@ describe('modus-wc-content-tree', () => { html: ``, }); - const instance = page.rootInstance as any; + const instance = page.rootInstance; const filterSpy = jest .spyOn(instance, 'filterNodes') .mockResolvedValue(undefined); - instance.handleInputChange({ + instance['handleInputChange']({ target: { value: 'hello' }, - } as any); + } as unknown as CustomEvent); // Wait slightly longer than debounce (150ms) await new Promise((r) => setTimeout(r, 180)); @@ -707,18 +712,18 @@ describe('modus-wc-content-tree', () => { html: ``, }); - const instance = page.rootInstance as any; + const instance = page.rootInstance; jest.spyOn(instance, 'filterNodes').mockResolvedValue(undefined); const clearSpy = jest.spyOn(window, 'clearTimeout'); // Force branch - instance.debounceTimer = 999; + instance['debounceTimer'] = 999; - instance.handleInputChange({ + instance['handleInputChange']({ target: { value: 'x' }, - } as any); + } as unknown as CustomEvent); expect(clearSpy).toHaveBeenCalledWith(999); }); diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx index 53e9a37d0c..0937ca18e1 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx @@ -14,7 +14,7 @@ export class ModusWcContentTree { private inheritedAttributes: Attributes = {}; private slotEl?: HTMLSlotElement; private debounceTimer?: number; - private cachedItems?: HTMLModusWcTreeItemElement[]; + private cachedItems?: ModusWcTreeItemElement[]; /** Reference to the host element */ @Element() el!: HTMLElement; @@ -74,7 +74,7 @@ export class ModusWcContentTree { } this.debounceTimer = window.setTimeout(() => { - this.filterNodes(this.searchValue); + void this.filterNodes(this.searchValue); }, 150); }; @@ -83,7 +83,7 @@ export class ModusWcContentTree { if (!this.cachedItems) { this.cachedItems = Array.from( this.el.querySelectorAll('modus-wc-tree-item') - ) as HTMLModusWcTreeItemElement[]; + ); } const menuItems = this.cachedItems; @@ -99,7 +99,7 @@ export class ModusWcContentTree { // First pass: identify matches and collect items to show/hide const matchingItems = new Set(); - const itemsToExpand: HTMLModusWcTreeItemElement[] = []; + const itemsToExpand: ModusWcTreeItemElement[] = []; const processedParents = new Set(); // Build match set efficiently @@ -127,7 +127,7 @@ export class ModusWcContentTree { ) { processedParents.add(parent); parent.style.display = ''; - itemsToExpand.push(parent as HTMLModusWcTreeItemElement); + itemsToExpand.push(parent as ModusWcTreeItemElement); } parent = parent.parentElement; } @@ -158,7 +158,7 @@ export class ModusWcContentTree { private handleInputKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { this.searchValue = ''; - this.filterNodes(''); + void this.filterNodes(''); } }; @@ -227,7 +227,7 @@ export class ModusWcContentTree { variant="borderless" size="sm" shape="circle" - onClick={this.toggleExpandCollapse} + onClick={() => void this.toggleExpandCollapse()} aria-label={ this.areAllExpanded ? 'Collapse all' : 'Expand all' } diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.spec.ts index 2edfc87c87..df95d92b84 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.spec.ts @@ -63,10 +63,7 @@ describe('modus-wc-tree-actions', () => { page.root?.addEventListener('treeActionClick', eventSpy); const treeActions = page.rootInstance; - treeActions['handleActionClick']( - actions[0], - new MouseEvent('click') as any - ); + treeActions['handleActionClick'](actions[0], new MouseEvent('click')); expect(eventSpy).not.toHaveBeenCalled(); }); @@ -89,12 +86,12 @@ describe('modus-wc-tree-actions', () => { const treeActions = page.rootInstance; expect(treeActions.isDropdownOpen).toBe(false); - treeActions['handleMoreActionsClick'](new MouseEvent('click') as any); + treeActions['handleMoreActionsClick'](new MouseEvent('click')); await page.waitForChanges(); expect(treeActions.isDropdownOpen).toBe(true); - treeActions['handleMoreActionsClick'](new MouseEvent('click') as any); + treeActions['handleMoreActionsClick'](new MouseEvent('click')); await page.waitForChanges(); expect(treeActions.isDropdownOpen).toBe(false); @@ -118,7 +115,7 @@ describe('modus-wc-tree-actions', () => { page.root?.addEventListener('dropdownOpened', eventSpy); const treeActions = page.rootInstance; - treeActions['handleMoreActionsClick'](new MouseEvent('click') as any); + treeActions['handleMoreActionsClick'](new MouseEvent('click')); await page.waitForChanges(); expect(eventSpy).toHaveBeenCalled(); @@ -162,7 +159,7 @@ describe('modus-wc-tree-actions', () => { const treeActions = page.rootInstance; treeActions.isDropdownOpen = true; - treeActions.moreActionsButton = null as any; + treeActions.moreActionsButton = null; const outsideElement = document.createElement('div'); const clickEvent = new MouseEvent('click', { bubbles: true }); @@ -277,7 +274,7 @@ describe('modus-wc-tree-actions', () => { }); const treeActions = page.rootInstance; - const initializerSpy = jest.spyOn(treeActions as any, 'initializePopper'); + const initializerSpy = jest.spyOn(treeActions, 'initializePopper' as never); treeActions.actions = actions; await page.waitForChanges(); @@ -297,7 +294,7 @@ describe('modus-wc-tree-actions', () => { const destroySpy = jest.fn(); treeActions.popperInstance = { destroy: destroySpy, - } as any; + } as unknown as ReturnType; treeActions.actions = [{ id: '1', icon: 'edit', label: 'Edit' }]; treeActions.componentDidUpdate(); @@ -440,10 +437,7 @@ describe('modus-wc-tree-actions', () => { const treeActions = page.rootInstance; treeActions.isDropdownOpen = true; - treeActions['handleActionClick']( - actions[1], - new MouseEvent('click') as any - ); + treeActions['handleActionClick'](actions[1], new MouseEvent('click')); await page.waitForChanges(); expect(treeActions.isDropdownOpen).toBe(false); @@ -456,8 +450,8 @@ describe('modus-wc-tree-actions', () => { }); const treeActions = page.rootInstance; - treeActions.moreActionsButton = null as any; - treeActions.moreActionsDropdown = null as any; + treeActions.moreActionsButton = null; + treeActions.moreActionsDropdown = null; treeActions['initializePopper'](); @@ -483,7 +477,9 @@ describe('modus-wc-tree-actions', () => { const mockPopper = { destroy: jest.fn(), }; - treeActions.popperInstance = mockPopper as any; + treeActions.popperInstance = mockPopper as unknown as ReturnType< + typeof import('@popperjs/core').createPopper + >; treeActions.moreActionsButton = page.root?.querySelector( '.modus-wc-tree-more-actions-wrapper modus-wc-button' @@ -509,7 +505,7 @@ describe('modus-wc-tree-actions', () => { const destroySpy = jest.fn(); treeActions.popperInstance = { destroy: destroySpy, - } as any; + } as unknown as ReturnType; treeActions.disconnectedCallback(); @@ -554,8 +550,8 @@ describe('modus-wc-tree-actions', () => { const treeActions = page.rootInstance; const handleActionClickSpy = jest.spyOn( - treeActions as any, - 'handleActionClick' + treeActions, + 'handleActionClick' as never ); const button = page.root?.querySelector('modus-wc-button'); @@ -588,10 +584,7 @@ describe('modus-wc-tree-actions', () => { treeActions.isDropdownOpen = true; await page.waitForChanges(); - const handleActionClickSpy = jest.spyOn( - treeActions as any, - 'handleActionClick' - ); + const handleActionClickSpy = jest.spyOn(treeActions, 'handleActionClick'); const dropdownAction = page.root?.querySelector( '.modus-wc-tree-dropdown-action' diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts index 3d87c6cc02..1659c62335 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts @@ -721,7 +721,7 @@ describe('modus-wc-tree-item', () => { page.root?.addEventListener('selectionsChange', eventSpy); const treeItem = page.rootInstance; - treeItem['handleCheckboxClick'](new MouseEvent('click') as any); + treeItem['handleCheckboxClick'](new MouseEvent('click')); await page.waitForChanges(); expect(eventSpy).toHaveBeenCalled(); @@ -740,7 +740,7 @@ describe('modus-wc-tree-item', () => { page.root?.addEventListener('selectionsChange', eventSpy); const treeItem = page.rootInstance; - treeItem['handleCheckboxClick'](new MouseEvent('click') as any); + treeItem['handleCheckboxClick'](new MouseEvent('click')); await page.waitForChanges(); expect(eventSpy).not.toHaveBeenCalled(); diff --git a/src/custom-elements.json b/src/custom-elements.json index fe94bcdc6e..71294091c8 100644 --- a/src/custom-elements.json +++ b/src/custom-elements.json @@ -8006,7 +8006,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable typography component used to render text with different sizes, hierarchy, and weights.\r\n\r\nNote: When using heading elements (h1-h6), the default heading CSS styling can be accessed without modifying\r\nthe default size (size=\"md\") and weight (weight=\"normal\") properties. Default styling can be overridden by\r\nproviding your own custom values for the size or weight properties from the available options.", + "description": "A customizable typography component used to render text with different sizes, hierarchy, and weights.\r\n\r\nNote:\r\n- When using heading elements (h1-h6), the default heading CSS styling can be accessed without modifying\r\nthe default size (size=\"md\") and weight (weight=\"normal\") properties. Default styling can be overridden by\r\nproviding your own custom values for the size or weight properties from the available options.\r\n\r\n\r\n- If both slot content and `label` are provided, only the slot content will be rendered\r\n- Use the `label` prop when you need to dynamically update the text.", "name": "ModusWCTypography", "members": [ { From f86ee07d131e8ab27094443a28ab1d776e83bcdd Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:54:10 +0530 Subject: [PATCH 20/39] 662 - build fixes --- .../modus-wc-content-tree.spec.ts | 29 +++++++++++++++++++ .../modus-wc-content-tree.tsx | 10 +++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts index d4d165a767..9e20a797c6 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts @@ -591,6 +591,35 @@ describe('modus-wc-content-tree', () => { expect(tree.areAllExpanded).toBe(false); }); + it('handleToggleClick calls toggleExpandCollapse', async () => { + const page = await newSpecPage({ + components: [ModusWcContentTree], + html: ` + + + + `, + }); + + const tree = page.rootInstance; + + // Mock the tree item methods + const item = page.root?.querySelector( + 'modus-wc-tree-item' + ) as ModusWcTreeItemElement; + item.expandSubTree = jest.fn().mockResolvedValue(undefined); + item.collapseSubTree = jest.fn().mockResolvedValue(undefined); + + const toggleSpy = jest.spyOn(tree, 'toggleExpandCollapse' as never); + + tree['handleToggleClick'](); + + // Wait for the async operation to complete + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(toggleSpy).toHaveBeenCalled(); + }); + it('filterNodes caches menu items on first call', async () => { const page = await newSpecPage({ components: [ModusWcContentTree], diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx index 0937ca18e1..265c494aeb 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx @@ -178,7 +178,11 @@ export class ModusWcContentTree { this.cachedItems = undefined; }; - private toggleExpandCollapse = async () => { + private handleToggleClick = (): void => { + void this.toggleExpandCollapse(); + }; + + private async toggleExpandCollapse(): Promise { const treeItems = this.el.querySelectorAll('modus-wc-tree-item'); this.areAllExpanded = !this.areAllExpanded; @@ -197,7 +201,7 @@ export class ModusWcContentTree { }); await Promise.all(promises); - }; + } render() { return ( @@ -227,7 +231,7 @@ export class ModusWcContentTree { variant="borderless" size="sm" shape="circle" - onClick={() => void this.toggleExpandCollapse()} + onClick={this.handleToggleClick} aria-label={ this.areAllExpanded ? 'Collapse all' : 'Expand all' } From c1f674a15b8d9b5f3283b3a3f9b2cb1a688c62b0 Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:28:05 +0530 Subject: [PATCH 21/39] 662 - fix PR comments --- .../modus-wc-content-tree.spec.ts.snap | 24 ------------------- .../modus-wc-content-tree.scss | 10 ++++---- .../modus-wc-content-tree.spec.ts | 13 ---------- .../modus-wc-tree-actions.tsx | 4 ++++ .../modus-wc-tree-view.spec.ts.snap | 5 ++-- .../modus-wc-tree-view.spec.ts | 7 +----- 6 files changed, 13 insertions(+), 50 deletions(-) diff --git a/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap b/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap index 5f26a01fc5..f82da8968f 100644 --- a/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap +++ b/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap @@ -1,29 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`modus-wc-content-tree should render with custom props 1`] = ` - - -
          -
          - -
          - - - -
          -
          -
          -
          - - -
          -
          -
          -
          -`; - exports[`modus-wc-content-tree should render with default props 1`] = ` diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index 53c3f2324d..648b0c92c4 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -65,10 +65,12 @@ modus-wc-content-tree { [data-theme='modus-classic-dark'], [data-theme='modus-modern-dark'], -[data-theme='connect-dark'] modus-wc-content-tree { - .modus-wc-content-tree-actions { - .modus-wc-content-tree-action-icon { - color: var(--modus-wc-color-gray-light); +[data-theme='connect-dark'] { + modus-wc-content-tree { + .modus-wc-content-tree-actions { + .modus-wc-content-tree-action-icon { + color: var(--modus-wc-color-gray-light); + } } } } diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts index 9e20a797c6..11388c579a 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts @@ -11,19 +11,6 @@ describe('modus-wc-content-tree', () => { expect(page.root).toMatchSnapshot(); }); - it('should render with custom props', async () => { - const page = await newSpecPage({ - components: [ModusWcContentTree], - html: ` - `, - }); - expect(page.root).toMatchSnapshot(); - }); - it('should filter nodes based on search input', async () => { const page = await newSpecPage({ components: [ModusWcContentTree], diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx index dc245df596..f15b4aa335 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx @@ -21,6 +21,10 @@ export interface ModusTreeItemActions { disabled?: boolean; // Optional flag to disable the action } +/** * ModusWcTreeActions is a component that renders action buttons for tree items in the Modus content tree. + * It supports displaying a primary action and grouping additional actions in a dropdown menu if there are more than two actions. + */ + @Component({ tag: 'modus-wc-tree-actions', styleUrl: 'modus-wc-tree-actions.scss', diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/__snapshots__/modus-wc-tree-view.spec.ts.snap b/src/components/modus-wc-content-tree/modus-wc-tree-view/__snapshots__/modus-wc-tree-view.spec.ts.snap index 12e5eb1cbc..6498f57b78 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-view/__snapshots__/modus-wc-tree-view.spec.ts.snap +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/__snapshots__/modus-wc-tree-view.spec.ts.snap @@ -8,10 +8,9 @@ exports[`modus-wc-tree-view renders as a sublist when isSubList is true 1`] = ` `; exports[`modus-wc-tree-view renders with custom props 1`] = ` - + -
            -
          +
            `; diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts index debf67bee1..620d678d9c 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts @@ -13,12 +13,7 @@ describe('modus-wc-tree-view', () => { it('renders with custom props', async () => { const page = await newSpecPage({ components: [ModusWcTreeView], - html: ` - `, + html: ``, }); expect(page.root).toMatchSnapshot(); }); From acab2fcc4d4f91b5dde760fea6be3ab82a790bed Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:42:54 +0530 Subject: [PATCH 22/39] 662 - address pr comments --- .../modus-wc-content-tree.spec.ts | 18 +++++++++--------- .../modus-wc-content-tree.tsx | 10 +++++----- .../modus-wc-tree-actions.tsx | 13 +++++-------- .../modus-wc-tree-item.spec.ts | 6 +++--- .../modus-wc-tree-item/modus-wc-tree-item.tsx | 16 +++++++--------- 5 files changed, 29 insertions(+), 34 deletions(-) diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts index 11388c579a..cd4d2c3f2a 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts @@ -1,6 +1,6 @@ import { newSpecPage } from '@stencil/core/testing'; import { ModusWcContentTree } from './modus-wc-content-tree'; -import { ModusWcTreeItemElement } from './modus-wc-tree-item/modus-wc-tree-item'; +import { ITreeItemElement } from './modus-wc-tree-item/modus-wc-tree-item'; describe('modus-wc-content-tree', () => { it('should render with default props', async () => { @@ -83,10 +83,10 @@ describe('modus-wc-content-tree', () => { const root = page.root!; const parent = root.querySelectorAll( 'modus-wc-tree-item' - )[0] as ModusWcTreeItemElement; + )[0] as ITreeItemElement; const child = root.querySelectorAll( 'modus-wc-tree-item' - )[1] as ModusWcTreeItemElement; + )[1] as ITreeItemElement; // Mock expandSubTree on parent const expandSubTreeMock = jest.fn().mockResolvedValue(undefined); @@ -137,7 +137,7 @@ describe('modus-wc-content-tree', () => { const tree = page.rootInstance; const item = page.root!.querySelector( 'modus-wc-tree-item' - ) as ModusWcTreeItemElement; + ) as ITreeItemElement; item.hasSubtree = true; const expandMock = jest.fn().mockResolvedValue(undefined); @@ -467,7 +467,7 @@ describe('modus-wc-content-tree', () => { const tree = page.rootInstance; const parent = page.root?.querySelector( 'modus-wc-tree-item' - ) as ModusWcTreeItemElement; + ) as ITreeItemElement; // Mock expandSubTree to reject with error const expandMock = jest.fn().mockRejectedValue(new Error('Expand failed')); @@ -497,7 +497,7 @@ describe('modus-wc-content-tree', () => { const tree = page.rootInstance; const parents = page.root?.querySelectorAll( 'modus-wc-tree-item[has-subtree]' - ) as NodeListOf; + ) as NodeListOf; // First parent fails const expandMock1 = jest.fn().mockRejectedValue(new Error('Failed')); @@ -568,7 +568,7 @@ describe('modus-wc-content-tree', () => { const item = page.root?.querySelector( 'modus-wc-tree-item' - ) as ModusWcTreeItemElement; + ) as ITreeItemElement; const collapseMock = jest.fn().mockResolvedValue(undefined); item.collapseSubTree = collapseMock; @@ -593,7 +593,7 @@ describe('modus-wc-content-tree', () => { // Mock the tree item methods const item = page.root?.querySelector( 'modus-wc-tree-item' - ) as ModusWcTreeItemElement; + ) as ITreeItemElement; item.expandSubTree = jest.fn().mockResolvedValue(undefined); item.collapseSubTree = jest.fn().mockResolvedValue(undefined); @@ -661,7 +661,7 @@ describe('modus-wc-content-tree', () => { const tree = page.rootInstance; const parent = page.root?.querySelector( 'modus-wc-tree-item' - ) as ModusWcTreeItemElement; + ) as ITreeItemElement; const expandMock = jest.fn().mockResolvedValue(undefined); parent.expandSubTree = expandMock; diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx index 265c494aeb..0964996949 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx @@ -1,6 +1,6 @@ import { Component, Element, h, Host, Prop, State } from '@stencil/core'; import { Attributes, inheritAriaAttributes } from '../utils'; -import { ModusWcTreeItemElement } from './modus-wc-tree-item/modus-wc-tree-item'; +import { ITreeItemElement } from './modus-wc-tree-item/modus-wc-tree-item'; /** * A customizable content tree component used to display hierarchical data in a tree structure. @@ -14,7 +14,7 @@ export class ModusWcContentTree { private inheritedAttributes: Attributes = {}; private slotEl?: HTMLSlotElement; private debounceTimer?: number; - private cachedItems?: ModusWcTreeItemElement[]; + private cachedItems?: ITreeItemElement[]; /** Reference to the host element */ @Element() el!: HTMLElement; @@ -99,7 +99,7 @@ export class ModusWcContentTree { // First pass: identify matches and collect items to show/hide const matchingItems = new Set(); - const itemsToExpand: ModusWcTreeItemElement[] = []; + const itemsToExpand: ITreeItemElement[] = []; const processedParents = new Set(); // Build match set efficiently @@ -127,7 +127,7 @@ export class ModusWcContentTree { ) { processedParents.add(parent); parent.style.display = ''; - itemsToExpand.push(parent as ModusWcTreeItemElement); + itemsToExpand.push(parent as ITreeItemElement); } parent = parent.parentElement; } @@ -187,7 +187,7 @@ export class ModusWcContentTree { this.areAllExpanded = !this.areAllExpanded; const promises = Array.from(treeItems).map((item) => { - const treeItem = item as ModusWcTreeItemElement; + const treeItem = item as ITreeItemElement; const hasSubtree = item.hasAttribute('has-subtree') || treeItem.hasSubtree === true; if (hasSubtree) { diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx index f15b4aa335..fb7d29b642 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx @@ -12,7 +12,7 @@ import { } from '@stencil/core'; import { ModusSize } from '../../types'; -export interface ModusTreeItemActions { +export interface ITreeItemActions { id: string; // Unique identifier for the action icon: string; // Icon name for the action, e.g., 'edit', 'trash' iconVariant?: 'solid' | 'outlined'; // Optional variant for the icon @@ -21,10 +21,10 @@ export interface ModusTreeItemActions { disabled?: boolean; // Optional flag to disable the action } -/** * ModusWcTreeActions is a component that renders action buttons for tree items in the Modus content tree. +/** + * ModusWcTreeActions is a component that renders action buttons for tree items in the Modus content tree. * It supports displaying a primary action and grouping additional actions in a dropdown menu if there are more than two actions. */ - @Component({ tag: 'modus-wc-tree-actions', styleUrl: 'modus-wc-tree-actions.scss', @@ -39,7 +39,7 @@ export class ModusWcTreeActions { @Element() el!: HTMLElement; /** List of actions to display */ - @Prop({ mutable: true }) actions?: ModusTreeItemActions[]; + @Prop({ mutable: true }) actions?: ITreeItemActions[]; /** The size of the action buttons and icons. */ @Prop() size: ModusSize = 'md'; @@ -85,10 +85,7 @@ export class ModusWcTreeActions { } } - private handleActionClick = ( - action: ModusTreeItemActions, - event: MouseEvent - ) => { + private handleActionClick = (action: ITreeItemActions, event: MouseEvent) => { event.stopPropagation(); if (action.disabled) return; diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts index 1659c62335..df76afecec 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts @@ -1,5 +1,5 @@ import { newSpecPage } from '@stencil/core/testing'; -import { ModusWcTreeItem, ModusWcTreeItemElement } from './modus-wc-tree-item'; +import { ModusWcTreeItem, ITreeItemElement } from './modus-wc-tree-item'; describe('modus-wc-tree-item', () => { it('renders with default props', async () => { @@ -342,7 +342,7 @@ describe('modus-wc-tree-item', () => { const parent = page.rootInstance; const children = page.root?.querySelectorAll( '.modus-wc-tree-dropdown modus-wc-tree-item' - ) as NodeListOf; + ) as NodeListOf; const checkbox = page.root?.querySelector('modus-wc-checkbox'); checkbox?.dispatchEvent(new MouseEvent('click', { bubbles: true })); @@ -524,7 +524,7 @@ describe('modus-wc-tree-item', () => { const submenu = page.root?.querySelector('.modus-wc-tree-dropdown'); const children = Array.from( submenu?.querySelectorAll('modus-wc-tree-item') || [] - ) as ModusWcTreeItemElement[]; + ) as ITreeItemElement[]; // First child with checkbox should be selected expect(children[0]?.selected).toBe(true); diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx index 6015fcc9aa..a0e9ac51ca 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx @@ -12,9 +12,9 @@ import { import { convertPropsToClasses } from './modus-wc-tree-item.tailwind'; import { ModusSize } from '../../types'; import { Attributes, inheritAriaAttributes } from '../../utils'; -import { ModusTreeItemActions } from '../modus-wc-tree-actions/modus-wc-tree-actions'; +import { ITreeItemActions } from '../modus-wc-tree-actions/modus-wc-tree-actions'; -export interface ModusWcTreeItemElement extends HTMLElement { +export interface ITreeItemElement extends HTMLElement { value: string; selected?: boolean; checkbox?: boolean; @@ -60,7 +60,7 @@ export class ModusWcTreeItem { @Prop() hasSubtree?: boolean; /** Actions to display for this tree item. */ - @Prop() treeItemActions?: ModusTreeItemActions[]; + @Prop() treeItemActions?: ITreeItemActions[]; /** The size of the tree item icons and actions. */ @Prop() size: ModusSize = 'sm'; @@ -198,9 +198,8 @@ export class ModusWcTreeItem { const childMenuItems = Array.from(submenu.children).filter( (el) => - el.tagName === 'MODUS-WC-TREE-ITEM' && - (el as ModusWcTreeItemElement).checkbox - ) as ModusWcTreeItemElement[]; + el.tagName === 'MODUS-WC-TREE-ITEM' && (el as ITreeItemElement).checkbox + ) as ITreeItemElement[]; let selectedCount = 0; @@ -222,7 +221,7 @@ export class ModusWcTreeItem { const descendants = Array.from( submenu.querySelectorAll('modus-wc-tree-item') - ) as ModusWcTreeItemElement[]; + ) as ITreeItemElement[]; descendants.forEach((item) => { if (!item.checkbox) return; @@ -257,7 +256,7 @@ export class ModusWcTreeItem { if (rootTreeView) { const allTreeItems = Array.from( rootTreeView.querySelectorAll('modus-wc-tree-item') - ) as ModusWcTreeItemElement[]; + ) as ITreeItemElement[]; const selectedValues = allTreeItems .filter((item) => item.checkbox && item.selected) .map((item) => item.value); @@ -270,7 +269,6 @@ export class ModusWcTreeItem { return (
          • Date: Wed, 25 Feb 2026 11:01:01 +0530 Subject: [PATCH 23/39] 662 - address pr comments --- .../modus-wc-tree-item/modus-wc-tree-item.spec.ts | 2 +- .../modus-wc-tree-item/modus-wc-tree-item.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts index df76afecec..4f84255247 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts @@ -1,5 +1,5 @@ import { newSpecPage } from '@stencil/core/testing'; -import { ModusWcTreeItem, ITreeItemElement } from './modus-wc-tree-item'; +import { ITreeItemElement, ModusWcTreeItem } from './modus-wc-tree-item'; describe('modus-wc-tree-item', () => { it('renders with default props', async () => { diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx index a0e9ac51ca..a0116872c0 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx @@ -74,7 +74,6 @@ export class ModusWcTreeItem { /** Event emitted when a tree item is selected. */ @StencilEvent({ bubbles: true, composed: true }) itemSelect!: EventEmitter<{ value: string; - selected?: boolean; }>; /** Event emitted when checkbox selection changes in multi-select mode. */ @@ -239,7 +238,7 @@ export class ModusWcTreeItem { private handleEmittedSelect = () => { if (this.checkbox) return; - this.itemSelect.emit({ value: this.value, selected: this.selected }); + this.itemSelect.emit({ value: this.value }); }; private handleCheckboxClick = () => { From 46e0a35da9fb91e6ce59cde7de6c0dc8421d9e08 Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:56:33 +0530 Subject: [PATCH 24/39] 662 - address pr comments --- src/components.d.ts | 42 ++++++--- src/components/modus-wc-button/readme.md | 4 +- .../modus-wc-content-tree.scss | 5 -- .../modus-wc-content-tree.stories.ts | 63 +++++++++++++ .../modus-wc-tree-actions.spec.ts | 88 ++++++++++++------- .../modus-wc-tree-actions.tsx | 44 ++++++---- .../modus-wc-tree-actions/readme.md | 13 ++- .../modus-wc-tree-item.spec.ts.snap | 16 ++-- .../modus-wc-tree-item.scss | 7 +- .../modus-wc-tree-item.spec.ts | 61 +++++++++++++ .../modus-wc-tree-item.tailwind.ts | 4 +- .../modus-wc-tree-item/modus-wc-tree-item.tsx | 31 ++++--- .../modus-wc-tree-item/readme.md | 34 +++---- src/components/modus-wc-date/readme.md | 38 ++++---- src/components/modus-wc-handle/readme.md | 22 +++-- src/components/modus-wc-icon/readme.md | 6 ++ .../modus-wc-input-feedback/readme.md | 2 + src/components/modus-wc-menu-item/readme.md | 2 +- src/components/modus-wc-modal/readme.md | 2 +- src/components/modus-wc-navbar/readme.md | 3 +- src/components/modus-wc-panel/readme.md | 2 +- src/components/modus-wc-toolbar/readme.md | 2 +- src/components/modus-wc-typography/readme.md | 7 +- src/custom-elements.json | 40 +++++---- 24 files changed, 377 insertions(+), 161 deletions(-) diff --git a/src/components.d.ts b/src/components.d.ts index 504a22237f..0b4887f160 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -21,8 +21,8 @@ import { SortingState } from "@tanstack/table-core"; import { ITab } from "./components/modus-wc-tabs/modus-wc-tabs"; import { IThemeConfig } from "./providers/theme/theme.types"; import { ToastPosition } from "./components/modus-wc-toast/modus-wc-toast"; -import { ModusTreeItemActions } from "./components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions"; -import { ModusTreeItemActions as ModusTreeItemActions1 } from "./components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions"; +import { ITreeItemActions } from "./components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions"; +import { ITreeItemActions as ITreeItemActions1 } from "./components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions"; import { TypographyHierarchy, TypographySize, TypographyWeight } from "./components/modus-wc-typography/modus-wc-typography"; export { AutocompleteTypes, DaisySize, Density, IAutocompleteItem, IAutocompleteNoResults, IInputFeedbackProp, ModusSize, Orientation, PopoverPlacement, TextFieldTypes, WeekStartDay } from "./components/types"; export { IBreadcrumb } from "./components/modus-wc-breadcrumbs/modus-wc-breadcrumbs"; @@ -40,8 +40,8 @@ export { SortingState } from "@tanstack/table-core"; export { ITab } from "./components/modus-wc-tabs/modus-wc-tabs"; export { IThemeConfig } from "./providers/theme/theme.types"; export { ToastPosition } from "./components/modus-wc-toast/modus-wc-toast"; -export { ModusTreeItemActions } from "./components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions"; -export { ModusTreeItemActions as ModusTreeItemActions1 } from "./components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions"; +export { ITreeItemActions } from "./components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions"; +export { ITreeItemActions as ITreeItemActions1 } from "./components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions"; export { TypographyHierarchy, TypographySize, TypographyWeight } from "./components/modus-wc-typography/modus-wc-typography"; export namespace Components { /** @@ -2020,15 +2020,19 @@ export namespace Components { */ "tooltipId"?: string; } + /** + * ModusWcTreeActions is a component that renders action buttons for tree items in the Modus content tree. + * It supports displaying a primary action and grouping additional actions in a dropdown menu if there are more than two actions. + */ interface ModusWcTreeActions { /** * List of actions to display */ - "actions"?: ModusTreeItemActions[]; + "actions"?: ITreeItemActions[]; /** * The size of the action buttons and icons. */ - "size": ModusSize; + "size": 'xs' | 'sm' | 'md' | 'lg'; } /** * A tree item component that represents a single node in a hierarchical tree structure. @@ -2069,11 +2073,11 @@ export namespace Components { /** * The size of the tree item icons and actions. */ - "size": ModusSize; + "size": 'xs' | 'sm' | 'md' | 'lg'; /** * Actions to display for this tree item. */ - "treeItemActions"?: ModusTreeItemActions1[]; + "treeItemActions"?: ITreeItemActions1[]; /** * The unique identifying value of the tree item. */ @@ -3154,6 +3158,10 @@ declare global { actionName: string; }; } + /** + * ModusWcTreeActions is a component that renders action buttons for tree items in the Modus content tree. + * It supports displaying a primary action and grouping additional actions in a dropdown menu if there are more than two actions. + */ interface HTMLModusWcTreeActionsElement extends Components.ModusWcTreeActions, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLModusWcTreeActionsElement, ev: ModusWcTreeActionsCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; @@ -3171,7 +3179,6 @@ declare global { interface HTMLModusWcTreeItemElementEventMap { "itemSelect": { value: string; - selected?: boolean; }; "selectionsChange": { selectedValues: string[]; @@ -5587,11 +5594,15 @@ declare namespace LocalJSX { */ "tooltipId"?: string; } + /** + * ModusWcTreeActions is a component that renders action buttons for tree items in the Modus content tree. + * It supports displaying a primary action and grouping additional actions in a dropdown menu if there are more than two actions. + */ interface ModusWcTreeActions { /** * List of actions to display */ - "actions"?: ModusTreeItemActions[]; + "actions"?: ITreeItemActions[]; /** * Event emitted when a dropdown is opened */ @@ -5606,7 +5617,7 @@ declare namespace LocalJSX { /** * The size of the action buttons and icons. */ - "size"?: ModusSize; + "size"?: 'xs' | 'sm' | 'md' | 'lg'; } /** * A tree item component that represents a single node in a hierarchical tree structure. @@ -5637,7 +5648,6 @@ declare namespace LocalJSX { */ "onItemSelect"?: (event: ModusWcTreeItemCustomEvent<{ value: string; - selected?: boolean; }>) => void; /** * Event emitted when checkbox selection changes in multi-select mode. @@ -5652,11 +5662,11 @@ declare namespace LocalJSX { /** * The size of the tree item icons and actions. */ - "size"?: ModusSize; + "size"?: 'xs' | 'sm' | 'md' | 'lg'; /** * Actions to display for this tree item. */ - "treeItemActions"?: ModusTreeItemActions1[]; + "treeItemActions"?: ITreeItemActions1[]; /** * The unique identifying value of the tree item. */ @@ -6012,6 +6022,10 @@ declare module "@stencil/core" { * When forceOpen is enabled, the tooltip will remain open and can only be closed by setting forceOpen to false. */ "modus-wc-tooltip": LocalJSX.ModusWcTooltip & JSXBase.HTMLAttributes; + /** + * ModusWcTreeActions is a component that renders action buttons for tree items in the Modus content tree. + * It supports displaying a primary action and grouping additional actions in a dropdown menu if there are more than two actions. + */ "modus-wc-tree-actions": LocalJSX.ModusWcTreeActions & JSXBase.HTMLAttributes; /** * A tree item component that represents a single node in a hierarchical tree structure. diff --git a/src/components/modus-wc-button/readme.md b/src/components/modus-wc-button/readme.md index 8e5a0dc1f1..bb3bf0959f 100644 --- a/src/components/modus-wc-button/readme.md +++ b/src/components/modus-wc-button/readme.md @@ -43,7 +43,8 @@ The component supports a `` for injecting content within the button, simil - [modus-wc-handle](../modus-wc-handle) - [modus-wc-modal](../modus-wc-modal) - [modus-wc-navbar](../modus-wc-navbar) - - [modus-wc-tree-actions](../modus-wc-content-tree/modus-wc-tree-actions) + - modus-wc-tree-actions + - [modus-wc-tree-item](../modus-wc-content-tree/modus-wc-tree-item) ### Graph ```mermaid @@ -57,6 +58,7 @@ graph TD; modus-wc-modal --> modus-wc-button modus-wc-navbar --> modus-wc-button modus-wc-tree-actions --> modus-wc-button + modus-wc-tree-item --> modus-wc-button style modus-wc-button fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index 648b0c92c4..7d0352c2d4 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -24,13 +24,8 @@ modus-wc-content-tree { } } - .modus-wc-content-tree-add-node { - margin-bottom: var(--modus-wc-spacing-md, 1rem); - } - .modus-wc-content-tree-content { align-items: center; - display: block; min-height: 500px; } diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts index a21b1f2599..99e1ffd693 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts @@ -91,6 +91,69 @@ export const Default: Story = { }, }; +export const EmptyState: Story = { + render: (args) => { + return html` + + + `; + }, +}; + +export const DisabledItems: Story = { + render: (args) => { + return html` + + + + + + + + + + + + + + + + + + `; + }, +}; + export const MultiSelect: Story = { render: (args) => { return html` diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.spec.ts index df95d92b84..b2867851be 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.spec.ts @@ -121,6 +121,61 @@ describe('modus-wc-tree-actions', () => { expect(eventSpy).toHaveBeenCalled(); }); + it('calls popperInstance.update when dropdown is opened and popper exists', async () => { + const actions = [ + { id: '1', icon: 'edit', label: 'Edit' }, + { id: '2', icon: 'delete', label: 'Delete' }, + { id: '3', icon: 'copy', label: 'Copy' }, + ]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + const treeActions = page.rootInstance; + const updateSpy = jest.fn(); + treeActions.popperInstance = { + update: updateSpy, + destroy: jest.fn(), + } as unknown as ReturnType; + + treeActions['handleMoreActionsClick'](new MouseEvent('click')); + await page.waitForChanges(); + + expect(updateSpy).toHaveBeenCalled(); + expect(treeActions.isDropdownOpen).toBe(true); + }); + + it('does not call popperInstance.update when popper is null', async () => { + const actions = [ + { id: '1', icon: 'edit', label: 'Edit' }, + { id: '2', icon: 'delete', label: 'Delete' }, + { id: '3', icon: 'copy', label: 'Copy' }, + ]; + + const page = await newSpecPage({ + components: [ModusWcTreeActions], + html: ``, + }); + + page.rootInstance.actions = actions; + await page.waitForChanges(); + + const treeActions = page.rootInstance; + treeActions.popperInstance = null; + + // Should not throw error + expect(() => { + treeActions['handleMoreActionsClick'](new MouseEvent('click')); + }).not.toThrow(); + + expect(treeActions.isDropdownOpen).toBe(true); + }); + it('closes dropdown when clicking outside', async () => { const actions = [ { id: '1', icon: 'edit', label: 'Edit' }, @@ -245,22 +300,6 @@ describe('modus-wc-tree-actions', () => { expect(treeActions.isDropdownOpen).toBe(true); }); - it('adds click event listener on componentDidLoad', async () => { - const page = await newSpecPage({ - components: [ModusWcTreeActions], - html: ``, - }); - - const addEventListenerSpy = jest.spyOn(document, 'addEventListener'); - - page.rootInstance.componentDidLoad(); - - expect(addEventListenerSpy).toHaveBeenCalledWith( - 'click', - expect.any(Function) - ); - }); - it('initializes popper when more than 2 actions on componentDidUpdate', async () => { const actions = [ { id: '1', icon: 'edit', label: 'Edit' }, @@ -279,8 +318,6 @@ describe('modus-wc-tree-actions', () => { treeActions.actions = actions; await page.waitForChanges(); - treeActions.componentDidUpdate(); - expect(initializerSpy).toHaveBeenCalled(); }); @@ -297,7 +334,6 @@ describe('modus-wc-tree-actions', () => { } as unknown as ReturnType; treeActions.actions = [{ id: '1', icon: 'edit', label: 'Edit' }]; - treeActions.componentDidUpdate(); expect(destroySpy).toHaveBeenCalled(); expect(treeActions.popperInstance).toBeNull(); @@ -493,13 +529,12 @@ describe('modus-wc-tree-actions', () => { expect(mockPopper.destroy).toHaveBeenCalled(); }); - it('removes event listener and destroys popper on disconnectedCallback when popper exists', async () => { + it('destroys popper on disconnectedCallback when popper exists', async () => { const page = await newSpecPage({ components: [ModusWcTreeActions], html: ``, }); - const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener'); const treeActions = page.rootInstance; const destroySpy = jest.fn(); @@ -509,31 +544,22 @@ describe('modus-wc-tree-actions', () => { treeActions.disconnectedCallback(); - expect(removeEventListenerSpy).toHaveBeenCalledWith( - 'click', - expect.any(Function) - ); expect(destroySpy).toHaveBeenCalled(); expect(treeActions.popperInstance).toBeNull(); }); - it('removes event listener on disconnectedCallback when popper is null', async () => { + it('disconnectedCallback sets popper to null when popper is null', async () => { const page = await newSpecPage({ components: [ModusWcTreeActions], html: ``, }); - const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener'); const treeActions = page.rootInstance; treeActions.popperInstance = null; treeActions.disconnectedCallback(); - expect(removeEventListenerSpy).toHaveBeenCalledWith( - 'click', - expect.any(Function) - ); expect(treeActions.popperInstance).toBeNull(); }); diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx index fb7d29b642..4deb9861ef 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx @@ -9,8 +9,8 @@ import { Prop, State, Event as StencilEvent, + Watch, } from '@stencil/core'; -import { ModusSize } from '../../types'; export interface ITreeItemActions { id: string; // Unique identifier for the action @@ -24,6 +24,7 @@ export interface ITreeItemActions { /** * ModusWcTreeActions is a component that renders action buttons for tree items in the Modus content tree. * It supports displaying a primary action and grouping additional actions in a dropdown menu if there are more than two actions. + * @internal */ @Component({ tag: 'modus-wc-tree-actions', @@ -39,10 +40,10 @@ export class ModusWcTreeActions { @Element() el!: HTMLElement; /** List of actions to display */ - @Prop({ mutable: true }) actions?: ITreeItemActions[]; + @Prop() actions?: ITreeItemActions[]; /** The size of the action buttons and icons. */ - @Prop() size: ModusSize = 'md'; + @Prop() size: 'xs' | 'sm' | 'md' | 'lg' = 'xs'; /** Internal state for dropdown visibility */ @State() isDropdownOpen: boolean = false; @@ -57,26 +58,32 @@ export class ModusWcTreeActions { }>; componentDidLoad() { - document.addEventListener('click', this.handleClickOutside); + this.updatePopperInstance(); } - componentDidUpdate() { - if (this.actions && this.actions.length > 2) { - this.initializePopper(); - } else if (this.popperInstance) { - this.popperInstance.destroy(); - this.popperInstance = null; - } + @Watch('actions') + onActionsChange() { + this.updatePopperInstance(); } disconnectedCallback() { - document.removeEventListener('click', this.handleClickOutside); if (this.popperInstance) { this.popperInstance.destroy(); this.popperInstance = null; } } + @Listen('click', { target: 'document' }) + handleClickOutside(event: MouseEvent) { + if (!this.isDropdownOpen) return; + + const target = event.target as HTMLElement; + const clickedButton = this.moreActionsButton?.contains(target); + if (!clickedButton) { + this.isDropdownOpen = false; + } + } + @Listen('dropdownOpened', { target: 'document' }) handleOtherDropdownOpened(event: CustomEvent) { // Close this dropdown if another one was opened @@ -111,13 +118,12 @@ export class ModusWcTreeActions { } }; - private handleClickOutside = (event: MouseEvent) => { - if (!this.isDropdownOpen) return; - - const target = event.target as HTMLElement; - const clickedButton = this.moreActionsButton?.contains(target); - if (!clickedButton) { - this.isDropdownOpen = false; + private updatePopperInstance = () => { + if (this.actions && this.actions.length > 2) { + this.initializePopper(); + } else if (this.popperInstance) { + this.popperInstance.destroy(); + this.popperInstance = null; } }; diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/readme.md b/src/components/modus-wc-content-tree/modus-wc-tree-actions/readme.md index 2a4795ed57..a13848e924 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/readme.md +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/readme.md @@ -5,12 +5,17 @@ +## Overview + +ModusWcTreeActions is a component that renders action buttons for tree items in the Modus content tree. +It supports displaying a primary action and grouping additional actions in a dropdown menu if there are more than two actions. + ## Properties -| Property | Attribute | Description | Type | Default | -| --------- | --------- | ----------------------------------------- | ------------------------------------- | ----------- | -| `actions` | `actions` | List of actions to display | `ModusTreeItemActions[] \| undefined` | `undefined` | -| `size` | `size` | The size of the action buttons and icons. | `"lg" \| "md" \| "sm"` | `'md'` | +| Property | Attribute | Description | Type | Default | +| --------- | --------- | ----------------------------------------- | --------------------------------- | ----------- | +| `actions` | `actions` | List of actions to display | `ITreeItemActions[] \| undefined` | `undefined` | +| `size` | `size` | The size of the action buttons and icons. | `"lg" \| "md" \| "sm" \| "xs"` | `'xs'` | ## Events diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/__snapshots__/modus-wc-tree-item.spec.ts.snap b/src/components/modus-wc-content-tree/modus-wc-tree-item/__snapshots__/modus-wc-tree-item.spec.ts.snap index 2beb290990..5a1fa6b645 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/__snapshots__/modus-wc-tree-item.spec.ts.snap +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/__snapshots__/modus-wc-tree-item.spec.ts.snap @@ -3,9 +3,11 @@ exports[`modus-wc-tree-item renders with checkbox 1`] = ` -
          • +
          • - + + +
            @@ -13,7 +15,7 @@ exports[`modus-wc-tree-item renders with checkbox 1`] = `
            - +
          • @@ -23,16 +25,18 @@ exports[`modus-wc-tree-item renders with checkbox 1`] = ` exports[`modus-wc-tree-item renders with default props 1`] = ` -
          • +
          • - + + +
            Test Item
            - +
          • diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss index 811175afb3..c0f7bfdc48 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss @@ -24,6 +24,11 @@ modus-wc-tree-item { position: absolute; } + .modus-wc-tree-toggle-btn { + background-color: transparent; + + } + .modus-wc-tree-toggle-button-hidden { visibility: hidden; } @@ -79,7 +84,7 @@ modus-wc-tree-item { &.modus-wc-tree-dropdown-show { display: block; - margin-inline-start: 1.5rem; + margin-inline-start: 1.6rem; } } diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts index 4f84255247..4e09db4c82 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts @@ -167,6 +167,47 @@ describe('modus-wc-tree-item', () => { ); }); + it('toggle button click expands/collapses subtree', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ` + +
            Child content
            +
            + `, + }); + + const submenu = page.root?.querySelector( + '.modus-wc-tree-dropdown' + ) as HTMLElement; + const button = page.root?.querySelector('modus-wc-button'); + + expect(submenu.classList.contains('modus-wc-tree-dropdown-show')).toBe( + false + ); + + // Simulate button click by emitting buttonClick event + const mouseEvent = new MouseEvent('click', { bubbles: true }); + const customEvent = new CustomEvent('buttonClick', { + detail: mouseEvent, + bubbles: true, + }); + button?.dispatchEvent(customEvent); + await page.waitForChanges(); + + expect(submenu.classList.contains('modus-wc-tree-dropdown-show')).toBe( + true + ); + + // Click again to collapse + button?.dispatchEvent(customEvent); + await page.waitForChanges(); + + expect(submenu.classList.contains('modus-wc-tree-dropdown-show')).toBe( + false + ); + }); + it('expandSubTree method expands the subtree', async () => { const page = await newSpecPage({ components: [ModusWcTreeItem], @@ -326,6 +367,26 @@ describe('modus-wc-tree-item', () => { expect(treeItem.selected).toBe(true); }); + it('checkbox uses correct size - xs converts to sm', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const checkbox = page.root?.querySelector('modus-wc-checkbox'); + expect(checkbox?.getAttribute('size')).toBe('sm'); + }); + + it('checkbox uses correct size - other sizes pass through', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const checkbox = page.root?.querySelector('modus-wc-checkbox'); + expect(checkbox?.getAttribute('size')).toBe('md'); + }); + it('updates children selection when parent checkbox is clicked', async () => { const page = await newSpecPage({ components: [ModusWcTreeItem], diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tailwind.ts b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tailwind.ts index ca3e10c19d..25e66b58e7 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tailwind.ts +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tailwind.ts @@ -1,5 +1,3 @@ -import { ModusSize } from '../../types'; - export const convertPropsToClasses = ({ disabled, selected, @@ -7,7 +5,7 @@ export const convertPropsToClasses = ({ }: { disabled?: boolean; selected?: boolean; - size?: ModusSize; + size?: 'xs' | 'sm' | 'md' | 'lg'; }): string => { let classes = ''; diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx index a0116872c0..83a3e163bf 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx @@ -10,7 +10,6 @@ import { Event as StencilEvent, } from '@stencil/core'; import { convertPropsToClasses } from './modus-wc-tree-item.tailwind'; -import { ModusSize } from '../../types'; import { Attributes, inheritAriaAttributes } from '../../utils'; import { ITreeItemActions } from '../modus-wc-tree-actions/modus-wc-tree-actions'; @@ -63,7 +62,7 @@ export class ModusWcTreeItem { @Prop() treeItemActions?: ITreeItemActions[]; /** The size of the tree item icons and actions. */ - @Prop() size: ModusSize = 'sm'; + @Prop() size: 'xs' | 'sm' | 'md' | 'lg' = 'xs'; /** Internal state to track if subtree is expanded */ @State() isExpanded: boolean = false; @@ -144,6 +143,7 @@ export class ModusWcTreeItem { const propClasses = convertPropsToClasses({ disabled: this.disabled, selected: this.selected, + size: this.size, }); if (propClasses) classList.push(propClasses); @@ -152,7 +152,7 @@ export class ModusWcTreeItem { return classList.join(' '); } - private handleToggleClick = (event: MouseEvent) => { + private handleToggleClick = (event: MouseEvent | KeyboardEvent) => { event.stopPropagation(); if (!this.hasSubtree) return; @@ -170,6 +170,7 @@ export class ModusWcTreeItem { private handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); + e.stopPropagation(); this.handleEmittedSelect(); } }; @@ -278,19 +279,29 @@ export class ModusWcTreeItem { {...this.inheritedAttributes} >
            - - + customClass="modus-wc-tree-toggle-btn" + aria-label={this.isExpanded ? 'Collapse' : 'Expand'} + disabled={!this.hasSubtree} + onButtonClick={(e) => { + this.handleToggleClick(e.detail); + }} + > + + {this.checkbox && ( { e.stopPropagation(); diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md b/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md index 0b66b5a693..8db9a72a1d 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md @@ -11,25 +11,25 @@ A tree item component that represents a single node in a hierarchical tree struc ## Properties -| Property | Attribute | Description | Type | Default | -| -------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ----------- | -| `checkbox` | `checkbox` | If true, renders a checkbox at the start of the tree item. | `boolean \| undefined` | `false` | -| `customClass` | `custom-class` | Custom CSS class to apply to the li element. | `string \| undefined` | `''` | -| `disabled` | `disabled` | The disabled state of the tree item. | `boolean \| undefined` | `undefined` | -| `hasSubtree` | `has-subtree` | Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior. | `boolean \| undefined` | `undefined` | -| `label` _(required)_ | `label` | The text label displayed for the tree item. | `string` | `undefined` | -| `selected` | `selected` | The selected state of the tree item. | `boolean \| undefined` | `undefined` | -| `size` | `size` | The size of the tree item icons and actions. | `"lg" \| "md" \| "sm"` | `'sm'` | -| `treeItemActions` | `tree-item-actions` | Actions to display for this tree item. | `ModusTreeItemActions[] \| undefined` | `undefined` | -| `value` | `value` | The unique identifying value of the tree item. | `string` | `''` | +| Property | Attribute | Description | Type | Default | +| -------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ----------- | +| `checkbox` | `checkbox` | If true, renders a checkbox at the start of the tree item. | `boolean \| undefined` | `false` | +| `customClass` | `custom-class` | Custom CSS class to apply to the li element. | `string \| undefined` | `''` | +| `disabled` | `disabled` | The disabled state of the tree item. | `boolean \| undefined` | `undefined` | +| `hasSubtree` | `has-subtree` | Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior. | `boolean \| undefined` | `undefined` | +| `label` _(required)_ | `label` | The text label displayed for the tree item. | `string` | `undefined` | +| `selected` | `selected` | The selected state of the tree item. | `boolean \| undefined` | `undefined` | +| `size` | `size` | The size of the tree item icons and actions. | `"lg" \| "md" \| "sm" \| "xs"` | `'xs'` | +| `treeItemActions` | `tree-item-actions` | Actions to display for this tree item. | `ITreeItemActions[] \| undefined` | `undefined` | +| `value` | `value` | The unique identifying value of the tree item. | `string` | `''` | ## Events -| Event | Description | Type | -| ------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------ | -| `itemSelect` | Event emitted when a tree item is selected. | `CustomEvent<{ value: string; selected?: boolean \| undefined; }>` | -| `selectionsChange` | Event emitted when checkbox selection changes in multi-select mode. | `CustomEvent<{ selectedValues: string[]; }>` | +| Event | Description | Type | +| ------------------ | ------------------------------------------------------------------- | -------------------------------------------- | +| `itemSelect` | Event emitted when a tree item is selected. | `CustomEvent<{ value: string; }>` | +| `selectionsChange` | Event emitted when checkbox selection changes in multi-select mode. | `CustomEvent<{ selectedValues: string[]; }>` | ## Methods @@ -59,13 +59,15 @@ Type: `Promise` ### Depends on +- [modus-wc-button](../../modus-wc-button) - [modus-wc-icon](../../modus-wc-icon) - [modus-wc-checkbox](../../modus-wc-checkbox) -- [modus-wc-tree-actions](../modus-wc-tree-actions) +- modus-wc-tree-actions ### Graph ```mermaid graph TD; + modus-wc-tree-item --> modus-wc-button modus-wc-tree-item --> modus-wc-icon modus-wc-tree-item --> modus-wc-checkbox modus-wc-tree-item --> modus-wc-tree-actions diff --git a/src/components/modus-wc-date/readme.md b/src/components/modus-wc-date/readme.md index e66d5454b0..eb8eb0251e 100644 --- a/src/components/modus-wc-date/readme.md +++ b/src/components/modus-wc-date/readme.md @@ -13,25 +13,25 @@ Adheres to WCAG 2.2 standards. ## Properties -| Property | Attribute | Description | Type | Default | -| ----------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -------------- | -| `bordered` | `bordered` | Indicates that the input should have a border. | `boolean \| undefined` | `true` | -| `customClass` | `custom-class` | Custom CSS class to apply to the input. | `string \| undefined` | `''` | -| `disabled` | `disabled` | Whether the form control is disabled. | `boolean \| undefined` | `false` | -| `feedback` | `feedback` | Feedback to render below the input. | `IInputFeedbackProp \| undefined` | `undefined` | -| `format` | `format` | The date format for display and input. | `"MMM DD, YYYY" \| "dd-mm-yyyy" \| "dd/mm/yyyy" \| "yyyy-mm-dd" \| "yyyy/mm/dd" \| undefined` | `'dd-mm-yyyy'` | -| `inputId` | `input-id` | The ID of the input element. | `string \| undefined` | `undefined` | -| `inputTabIndex` | `input-tab-index` | Determine the control's relative ordering for sequential focus navigation (typically with the Tab key). | `number \| undefined` | `undefined` | -| `label` | `label` | The text to display within the label. | `string \| undefined` | `undefined` | -| `max` | `max` | Maximum date value. | `string \| undefined` | `undefined` | -| `min` | `min` | Minimum date value. | `string \| undefined` | `undefined` | -| `name` | `name` | Name of the form control. Submitted with the form as part of a name/value pair. | `string \| undefined` | `undefined` | -| `readOnly` | `read-only` | Whether the value is editable. | `boolean \| undefined` | `false` | -| `required` | `required` | A value is required or must be checked for the form to be submittable. | `boolean \| undefined` | `false` | -| `showWeekNumbers` | `show-week-numbers` | Displays ISO 8601 week numbers in the calendar.Week numbers are calculated with Monday as the first day of the week. | `boolean \| undefined` | `false` | -| `size` | `size` | The size of the input. | `"lg" \| "md" \| "sm" \| undefined` | `'md'` | -| `value` | `value` | The value of the control. | `string` | `''` | -| `weekStartDay` | `week-start-day` | The first day of the week for the calendar display | `"friday" \| "monday" \| "saturday" \| "sunday" \| "thursday" \| "tuesday" \| "wednesday" \| undefined` | `'sunday'` | +| Property | Attribute | Description | Type | Default | +| ----------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -------------- | +| `bordered` | `bordered` | Indicates that the input should have a border. | `boolean \| undefined` | `true` | +| `customClass` | `custom-class` | Custom CSS class to apply to the input. | `string \| undefined` | `''` | +| `disabled` | `disabled` | Whether the form control is disabled. | `boolean \| undefined` | `false` | +| `feedback` | `feedback` | Feedback to render below the input. | `IInputFeedbackProp \| undefined` | `undefined` | +| `format` | `format` | The date format for display and input. | `"MMM DD, YYYY" \| "dd-mm-yyyy" \| "dd/mm/yyyy" \| "mm-dd-yyyy" \| "mm/dd/yyyy" \| "yyyy-mm-dd" \| "yyyy/mm/dd" \| undefined` | `'dd-mm-yyyy'` | +| `inputId` | `input-id` | The ID of the input element. | `string \| undefined` | `undefined` | +| `inputTabIndex` | `input-tab-index` | Determine the control's relative ordering for sequential focus navigation (typically with the Tab key). | `number \| undefined` | `undefined` | +| `label` | `label` | The text to display within the label. | `string \| undefined` | `undefined` | +| `max` | `max` | Maximum date value. | `string \| undefined` | `undefined` | +| `min` | `min` | Minimum date value. | `string \| undefined` | `undefined` | +| `name` | `name` | Name of the form control. Submitted with the form as part of a name/value pair. | `string \| undefined` | `undefined` | +| `readOnly` | `read-only` | Whether the value is editable. | `boolean \| undefined` | `false` | +| `required` | `required` | A value is required or must be checked for the form to be submittable. | `boolean \| undefined` | `false` | +| `showWeekNumbers` | `show-week-numbers` | Displays ISO 8601 week numbers in the calendar.Week numbers are calculated with Monday as the first day of the week. | `boolean \| undefined` | `false` | +| `size` | `size` | The size of the input. | `"lg" \| "md" \| "sm" \| undefined` | `'md'` | +| `value` | `value` | The value of the control. | `string` | `''` | +| `weekStartDay` | `week-start-day` | The first day of the week for the calendar display | `"friday" \| "monday" \| "saturday" \| "sunday" \| "thursday" \| "tuesday" \| "wednesday" \| undefined` | `'sunday'` | ## Events diff --git a/src/components/modus-wc-handle/readme.md b/src/components/modus-wc-handle/readme.md index d2128d1130..c79d679ec5 100644 --- a/src/components/modus-wc-handle/readme.md +++ b/src/components/modus-wc-handle/readme.md @@ -11,15 +11,19 @@ A draggable handle component for resizing adjacent elements ## Properties -| Property | Attribute | Description | Type | Default | -| ------------- | -------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------ | --------------- | -| `customClass` | `custom-class` | Custom CSS class to apply to the handle element. | `string \| undefined` | `''` | -| `density` | `density` | The density/spacing of the handle container (compact: 8px, comfortable: 12px, relaxed: 16px). | `"comfortable" \| "compact" \| "relaxed" \| undefined` | `'comfortable'` | -| `leftTarget` | `left-target` | The left target element to resize (CSS selector or HTMLElement) | `HTMLElement \| string \| undefined` | `undefined` | -| `orientation` | `orientation` | The orientation of the handle. | `"horizontal" \| "vertical" \| undefined` | `'horizontal'` | -| `rightTarget` | `right-target` | The right target element to resize (CSS selector or HTMLElement) | `HTMLElement \| string \| undefined` | `undefined` | -| `size` | `size` | The size of the handle. | `"2xl" \| "default" \| "lg" \| "xl" \| undefined` | `'default'` | -| `type` | `type` | The type of handle to display. | `"bar" \| "button" \| undefined` | `'bar'` | +| Property | Attribute | Description | Type | Default | +| --------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | --------------- | +| `buttonColor` | `button-color` | The color of the button. | `"danger" \| "primary" \| "secondary" \| "tertiary" \| "warning" \| undefined` | `'tertiary'` | +| `buttonSize` | `button-size` | The size of the button. | `"lg" \| "md" \| "sm" \| "xs" \| undefined` | `'md'` | +| `buttonVariant` | `button-variant` | The variant of the button. | `"borderless" \| "filled" \| "outlined" \| undefined` | `'filled'` | +| `customClass` | `custom-class` | Custom CSS class to apply to the handle element. | `string \| undefined` | `''` | +| `defaultSplit` | `default-split` | The initial split percentage for the left/top panel (1-100). The right/bottom panel gets the remaining percentage. | `number \| undefined` | `50` | +| `density` | `density` | The density/spacing of the handle container (compact: 8px, comfortable: 12px, relaxed: 16px). | `"comfortable" \| "compact" \| "relaxed" \| undefined` | `'comfortable'` | +| `leftTarget` | `left-target` | The left target element to resize (CSS selector or HTMLElement) | `HTMLElement \| string \| undefined` | `undefined` | +| `orientation` | `orientation` | The orientation of the handle. | `"horizontal" \| "vertical" \| undefined` | `'horizontal'` | +| `rightTarget` | `right-target` | The right target element to resize (CSS selector or HTMLElement) | `HTMLElement \| string \| undefined` | `undefined` | +| `size` | `size` | The size of the handle. | `"2xl" \| "default" \| "lg" \| "xl" \| undefined` | `'default'` | +| `type` | `type` | The type of handle to display. | `"bar" \| "button" \| undefined` | `'bar'` | ## Dependencies diff --git a/src/components/modus-wc-icon/readme.md b/src/components/modus-wc-icon/readme.md index 8d9709a927..915972cc79 100644 --- a/src/components/modus-wc-icon/readme.md +++ b/src/components/modus-wc-icon/readme.md @@ -33,9 +33,12 @@ A customizable icon component used to render Modus icons. - [modus-wc-content-tree](../modus-wc-content-tree) - [modus-wc-date](../modus-wc-date) - [modus-wc-file-dropzone](../modus-wc-file-dropzone) + - [modus-wc-handle](../modus-wc-handle) - [modus-wc-input-feedback](../modus-wc-input-feedback) - [modus-wc-table](../modus-wc-table) - [modus-wc-tabs](../modus-wc-tabs) + - modus-wc-tree-actions + - [modus-wc-tree-item](../modus-wc-content-tree/modus-wc-tree-item) ### Graph ```mermaid @@ -47,9 +50,12 @@ graph TD; modus-wc-content-tree --> modus-wc-icon modus-wc-date --> modus-wc-icon modus-wc-file-dropzone --> modus-wc-icon + modus-wc-handle --> modus-wc-icon modus-wc-input-feedback --> modus-wc-icon modus-wc-table --> modus-wc-icon modus-wc-tabs --> modus-wc-icon + modus-wc-tree-actions --> modus-wc-icon + modus-wc-tree-item --> modus-wc-icon style modus-wc-icon fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/src/components/modus-wc-input-feedback/readme.md b/src/components/modus-wc-input-feedback/readme.md index 1a30331d69..73741634d7 100644 --- a/src/components/modus-wc-input-feedback/readme.md +++ b/src/components/modus-wc-input-feedback/readme.md @@ -26,6 +26,7 @@ A customizable feedback component used to provide additional context related to ### Used by + - [modus-wc-autocomplete](../modus-wc-autocomplete) - [modus-wc-date](../modus-wc-date) - [modus-wc-number-input](../modus-wc-number-input) - [modus-wc-select](../modus-wc-select) @@ -41,6 +42,7 @@ A customizable feedback component used to provide additional context related to ```mermaid graph TD; modus-wc-input-feedback --> modus-wc-icon + modus-wc-autocomplete --> modus-wc-input-feedback modus-wc-date --> modus-wc-input-feedback modus-wc-number-input --> modus-wc-input-feedback modus-wc-select --> modus-wc-input-feedback diff --git a/src/components/modus-wc-menu-item/readme.md b/src/components/modus-wc-menu-item/readme.md index cf91090585..08477c2f93 100644 --- a/src/components/modus-wc-menu-item/readme.md +++ b/src/components/modus-wc-menu-item/readme.md @@ -9,7 +9,7 @@ A customizable menu item component used to display the item portion of a menu. -The component supports a `` called 'start-icon' for custom icons at the start of the item. +This component supports a 'start-icon' `` that allows for custom icons to be placed at the beginning of the item. ## Properties diff --git a/src/components/modus-wc-modal/readme.md b/src/components/modus-wc-modal/readme.md index 51d13ced16..49ac30e594 100644 --- a/src/components/modus-wc-modal/readme.md +++ b/src/components/modus-wc-modal/readme.md @@ -9,7 +9,7 @@ A customizable modal component used to display content in a dialog. -The component supports `` called 'header', 'content', and 'footer' for injecting custom HTML. +This component supports 'header', 'content', and 'footer' `` elements for inserting custom HTML. ## Properties diff --git a/src/components/modus-wc-navbar/readme.md b/src/components/modus-wc-navbar/readme.md index 9a52f6357e..b2c95f41a7 100644 --- a/src/components/modus-wc-navbar/readme.md +++ b/src/components/modus-wc-navbar/readme.md @@ -9,8 +9,7 @@ A customizable navbar component used for top level navigation of all Trimble applications. -The component supports a `` called 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. -It also supports `` called 'start', 'center', and 'end' for injecting additional custom HTML. +The component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML. ## Properties diff --git a/src/components/modus-wc-panel/readme.md b/src/components/modus-wc-panel/readme.md index 8d0c3f1255..d2fee2246d 100644 --- a/src/components/modus-wc-panel/readme.md +++ b/src/components/modus-wc-panel/readme.md @@ -9,7 +9,7 @@ A customizable panel component used to organize content in a structured layout. -The component supports `` called 'header', 'body', and 'footer' for injecting custom HTML. +This component provides 'header', 'body', and 'footer' `` elements for inserting custom HTML. ## Properties diff --git a/src/components/modus-wc-toolbar/readme.md b/src/components/modus-wc-toolbar/readme.md index 4510920c92..970e4e6d4f 100644 --- a/src/components/modus-wc-toolbar/readme.md +++ b/src/components/modus-wc-toolbar/readme.md @@ -9,7 +9,7 @@ A customizable toolbar component used to organize content across the entire page. -The component supports `` called `start`, `center`, and `end` for injecting custom HTML. +This component provides 'start', 'center', and 'end' `` elements for inserting custom HTML. ## Properties diff --git a/src/components/modus-wc-typography/readme.md b/src/components/modus-wc-typography/readme.md index 1859e14f68..04d961cd5e 100644 --- a/src/components/modus-wc-typography/readme.md +++ b/src/components/modus-wc-typography/readme.md @@ -9,10 +9,15 @@ A customizable typography component used to render text with different sizes, hierarchy, and weights. -Note: When using heading elements (h1-h6), the default heading CSS styling can be accessed without modifying +Note: +- When using heading elements (h1-h6), the default heading CSS styling can be accessed without modifying the default size (size="md") and weight (weight="normal") properties. Default styling can be overridden by providing your own custom values for the size or weight properties from the available options. + +- If both slot content and `label` are provided, only the slot content will be rendered +- Use the `label` prop when you need to dynamically update the text. + ## Properties | Property | Attribute | Description | Type | Default | diff --git a/src/custom-elements.json b/src/custom-elements.json index 71294091c8..3737a0804d 100644 --- a/src/custom-elements.json +++ b/src/custom-elements.json @@ -2377,7 +2377,7 @@ "declarations": [ { "kind": "class", - "description": "", + "description": "ModusWcTreeActions is a component that renders action buttons for tree items in the Modus content tree.\r\nIt supports displaying a primary action and grouping additional actions in a dropdown menu if there are more than two actions.", "name": "ModusWcTreeActions", "members": [ { @@ -2388,6 +2388,18 @@ }, "description": "Reference to the host element" }, + { + "kind": "method", + "name": "handleClickOutside", + "parameters": [ + { + "name": "event", + "type": { + "text": "MouseEvent" + } + } + ] + }, { "kind": "method", "name": "handleOtherDropdownOpened", @@ -2409,6 +2421,10 @@ "default": "false", "description": "Internal state for dropdown visibility" }, + { + "kind": "method", + "name": "onActionsChange" + }, { "kind": "method", "name": "render" @@ -2420,16 +2436,16 @@ "fieldName": "actions", "description": "List of actions to display", "type": { - "text": "ModusTreeItemActions[]" + "text": "ITreeItemActions[]" } }, { "name": "size", "fieldName": "size", - "default": "'md'", + "default": "'xs'", "description": "The size of the action buttons and icons.", "type": { - "text": "ModusSize" + "text": "'xs' | 'sm' | 'md' | 'lg'" } } ], @@ -2456,14 +2472,6 @@ } ], "exports": [ - { - "kind": "js", - "name": "ModusWcTreeActions", - "declaration": { - "name": "ModusWcTreeActions", - "module": "src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx" - } - }, { "kind": "custom-element-definition", "name": "modus-wc-tree-actions", @@ -2588,10 +2596,10 @@ { "name": "size", "fieldName": "size", - "default": "'sm'", + "default": "'xs'", "description": "The size of the tree item icons and actions.", "type": { - "text": "ModusSize" + "text": "'xs' | 'sm' | 'md' | 'lg'" } }, { @@ -2599,7 +2607,7 @@ "fieldName": "treeItemActions", "description": "Actions to display for this tree item.", "type": { - "text": "ModusTreeItemActions[]" + "text": "ITreeItemActions[]" } }, { @@ -2618,7 +2626,7 @@ "kind": "field", "name": "itemSelect", "type": { - "text": "EventEmitter<{\r\n value: string;\r\n selected?: boolean;\r\n }>" + "text": "EventEmitter<{\r\n value: string;\r\n }>" }, "description": "Event emitted when a tree item is selected." }, From 2e2603e113ef95be246ae6f47fe15b0c8103de1b Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:31:17 +0530 Subject: [PATCH 25/39] 662 - address pr comments --- .../modus-wc-tree-actions.scss | 2 +- .../modus-wc-tree-actions.tsx | 2 +- .../modus-wc-tree-item.spec.ts.snap | 2 +- .../modus-wc-tree-item.spec.ts | 42 +++++++++++-------- .../modus-wc-tree-item/modus-wc-tree-item.tsx | 23 +++++----- 5 files changed, 38 insertions(+), 33 deletions(-) diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss index fd46b1f106..8501e8c367 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss @@ -41,7 +41,7 @@ modus-wc-tree-actions { align-items: center; background: transparent; border: none; - color: var(--modus-wc-color-base-content-high-contrast); + color: var(--modus-wc-color-base-content-hight-contrast); cursor: pointer; display: flex; gap: var(--modus-wc-spacing-sm, 0.5rem); diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx index 4deb9861ef..601847057d 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx @@ -142,7 +142,7 @@ export class ModusWcTreeActions { this.moreActionsDropdown, { placement: 'bottom', - strategy: 'absolute', + strategy: 'fixed', modifiers: [ { name: 'offset', diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/__snapshots__/modus-wc-tree-item.spec.ts.snap b/src/components/modus-wc-content-tree/modus-wc-tree-item/__snapshots__/modus-wc-tree-item.spec.ts.snap index 5a1fa6b645..9e1ca7fd3b 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/__snapshots__/modus-wc-tree-item.spec.ts.snap +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/__snapshots__/modus-wc-tree-item.spec.ts.snap @@ -8,7 +8,7 @@ exports[`modus-wc-tree-item renders with checkbox 1`] = ` - +
            Test Item diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts index 4e09db4c82..42ce9f9d18 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts @@ -76,23 +76,6 @@ describe('modus-wc-tree-item', () => { expect(itemSelectSpy.mock.calls[0][0].detail.value).toBe('test-value'); }); - it('does not emit itemSelect when checkbox is enabled', async () => { - const page = await newSpecPage({ - components: [ModusWcTreeItem], - html: ``, - }); - - const itemSelectSpy = jest.fn(); - page.root?.addEventListener('itemSelect', itemSelectSpy); - - const li = page.root?.querySelector('li'); - li?.click(); - - await page.waitForChanges(); - - expect(itemSelectSpy).not.toHaveBeenCalled(); - }); - it('handles Enter key to emit itemSelect', async () => { const page = await newSpecPage({ components: [ModusWcTreeItem], @@ -539,6 +522,31 @@ describe('modus-wc-tree-item', () => { expect(parent.isIndeterminate).toBe(false); }); + it('updateIndeterminateState returns early when no checkbox items in children', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ` + +
              + +
            +
            + `, + }); + + const parent = page.rootInstance; + const event = new CustomEvent('selectionsChange', { bubbles: true }); + Object.defineProperty(event, 'target', { + value: page.root?.querySelector('modus-wc-tree-item[value="child"]'), + enumerable: true, + }); + + parent['updateIndeterminateState'](event); + + expect(parent.isIndeterminate).toBe(false); + // expect(parent.selected).toBe(false); + }); + it('updateChildrenSelection returns early when no subtree', async () => { const page = await newSpecPage({ components: [ModusWcTreeItem], diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx index 83a3e163bf..fb67608519 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx @@ -196,21 +196,19 @@ export class ModusWcTreeItem { const submenu = this.el.querySelector('.modus-wc-tree-dropdown'); if (!submenu) return; - const childMenuItems = Array.from(submenu.children).filter( - (el) => - el.tagName === 'MODUS-WC-TREE-ITEM' && (el as ITreeItemElement).checkbox + const descendants = Array.from( + submenu.querySelectorAll('modus-wc-tree-item') ) as ITreeItemElement[]; + const checkboxItems = descendants.filter((item) => item.checkbox); - let selectedCount = 0; - - childMenuItems.forEach((item) => { - if (item.selected) selectedCount++; - }); + if (!checkboxItems.length) return; + const selectedCount = checkboxItems.filter((item) => item.selected).length; - this.isIndeterminate = - selectedCount > 0 && selectedCount < childMenuItems.length; + const someSelected = selectedCount > 0; + const allSelected = selectedCount === checkboxItems.length; - this.selected = selectedCount === childMenuItems.length; + this.selected = allSelected; + this.isIndeterminate = someSelected && !allSelected; }; private updateChildrenSelection = (selected: boolean) => { @@ -238,7 +236,6 @@ export class ModusWcTreeItem { }; private handleEmittedSelect = () => { - if (this.checkbox) return; this.itemSelect.emit({ value: this.value }); }; @@ -298,7 +295,7 @@ export class ModusWcTreeItem { {this.checkbox && ( Date: Fri, 27 Feb 2026 15:11:35 +0530 Subject: [PATCH 26/39] 662 - fix selection state logic --- .../modus-wc-content-tree.scss | 2 +- .../modus-wc-tree-item.scss | 1 - .../modus-wc-tree-view.spec.ts | 136 ++++++++++++++++++ .../modus-wc-tree-view/modus-wc-tree-view.tsx | 12 ++ 4 files changed, 149 insertions(+), 2 deletions(-) diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index 7d0352c2d4..c7be27da12 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -29,7 +29,7 @@ modus-wc-content-tree { min-height: 500px; } - li.modus-wc-tree-item-selected:not(.modus-wc-tree-dropdown-show li) { + li.modus-wc-tree-item-li-active:not(.modus-wc-tree-dropdown-show li) { border-inline-start: 2px solid var(--modus-wc-color-primary); } diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss index c0f7bfdc48..9608ca4494 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss @@ -26,7 +26,6 @@ modus-wc-tree-item { .modus-wc-tree-toggle-btn { background-color: transparent; - } .modus-wc-tree-toggle-button-hidden { diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts index 620d678d9c..5c92b9ccec 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts @@ -243,4 +243,140 @@ describe('modus-wc-tree-view', () => { expect(classes).toContain('modus-wc-tree-dropdown'); expect(classes).toContain('my-custom-sublist'); }); + + it('handles itemSelect event and adds class to li element', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeView], + html: ` + + +
          • +
            Item 1
            +
          • +
            + +
          • +
            Item 2
            +
          • +
            +
            + `, + }); + + const treeView = page.rootInstance; + const firstTreeItem = page.root?.querySelector( + 'modus-wc-tree-item' + ) as HTMLElement; + const firstLi = firstTreeItem.querySelector('li'); + + const event = new CustomEvent('itemSelect', { + detail: { value: 'item1' }, + bubbles: true, + }); + Object.defineProperty(event, 'target', { + value: firstTreeItem, + enumerable: true, + }); + + treeView.handleItemSelect(event); + await page.waitForChanges(); + + expect(firstLi?.classList.contains('modus-wc-tree-item-li-active')).toBe( + true + ); + }); + + it('handles itemSelect event and removes class from previously selected li', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeView], + html: ` + + +
          • +
            Item 1
            +
          • +
            + +
          • +
            Item 2
            +
          • +
            +
            + `, + }); + + const treeView = page.rootInstance; + const treeItems = page.root?.querySelectorAll('modus-wc-tree-item'); + const firstTreeItem = treeItems?.[0] as HTMLElement; + const secondTreeItem = treeItems?.[1] as HTMLElement; + const firstLi = firstTreeItem.querySelector('li'); + const secondLi = secondTreeItem.querySelector('li'); + + // Select first item + const event1 = new CustomEvent('itemSelect', { + detail: { value: 'item1' }, + bubbles: true, + }); + Object.defineProperty(event1, 'target', { + value: firstTreeItem, + enumerable: true, + }); + + treeView.handleItemSelect(event1); + await page.waitForChanges(); + + expect(firstLi?.classList.contains('modus-wc-tree-item-li-active')).toBe( + true + ); + + // Select second item + const event2 = new CustomEvent('itemSelect', { + detail: { value: 'item2' }, + bubbles: true, + }); + Object.defineProperty(event2, 'target', { + value: secondTreeItem, + enumerable: true, + }); + + treeView.handleItemSelect(event2); + await page.waitForChanges(); + + // First li should no longer have the class + expect(firstLi?.classList.contains('modus-wc-tree-item-li-active')).toBe( + false + ); + // Second li should have the class + expect(secondLi?.classList.contains('modus-wc-tree-item-li-active')).toBe( + true + ); + }); + + it('handles itemSelect event when content element has no parent li', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeView], + html: ` + +
            +
            Item without li
            +
            +
            + `, + }); + + const treeView = page.rootInstance; + const item = page.root?.querySelector('.modus-wc-tree-item') as HTMLElement; + + const event = new CustomEvent('itemSelect', { + detail: { value: 'item1' }, + bubbles: true, + }); + Object.defineProperty(event, 'target', { + value: item, + enumerable: true, + }); + + // Should not throw when parent is not an li element + expect(() => treeView.handleItemSelect(event)).not.toThrow(); + }); }); diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx index 14b92cc08d..b905d9743d 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx @@ -31,6 +31,8 @@ export class ModusWcTreeView { const target = event.target as HTMLElement; if (!target) return; + console.log('target', target); + const allContents = this.el.querySelectorAll('.modus-wc-tree-content'); allContents.forEach((content) => content.classList.remove('modus-wc-tree-item-active') @@ -38,6 +40,16 @@ export class ModusWcTreeView { const targetContent = target.querySelector('.modus-wc-tree-content'); targetContent?.classList.add('modus-wc-tree-item-active'); + + const allTreeItemLis = this.el.querySelectorAll('modus-wc-tree-item > li'); + allTreeItemLis.forEach((li) => + li.classList.remove('modus-wc-tree-item-li-active') + ); + + const targetLi = targetContent?.parentElement; + if (targetLi?.tagName === 'LI') { + targetLi.classList.add('modus-wc-tree-item-li-active'); + } } private getClasses(): string { From 5c183d40c2a5c0f555839255fb01ffbd9a2cdbfe Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:18:04 +0530 Subject: [PATCH 27/39] 662 - multi seletect fix --- src/components.d.ts | 8 ++ .../modus-wc-content-tree.scss | 2 +- .../modus-wc-tree-item.scss | 2 +- .../modus-wc-tree-item.spec.ts | 58 ++++----- .../modus-wc-tree-item/modus-wc-tree-item.tsx | 34 ++++-- .../modus-wc-tree-item/readme.md | 1 + .../modus-wc-tree-view.spec.ts | 115 ------------------ .../modus-wc-tree-view/modus-wc-tree-view.tsx | 24 ++-- src/custom-elements.json | 8 ++ 9 files changed, 78 insertions(+), 174 deletions(-) diff --git a/src/components.d.ts b/src/components.d.ts index 0b4887f160..69b0bce1b4 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -2042,6 +2042,10 @@ export namespace Components { * If true, renders a checkbox at the start of the tree item. */ "checkbox"?: boolean; + /** + * The checked state of the tree item when checkbox is enabled. + */ + "checked"?: boolean; /** * Public method to collapse the subtree if it's expanded */ @@ -5627,6 +5631,10 @@ declare namespace LocalJSX { * If true, renders a checkbox at the start of the tree item. */ "checkbox"?: boolean; + /** + * The checked state of the tree item when checkbox is enabled. + */ + "checked"?: boolean; /** * Custom CSS class to apply to the li element. */ diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index c7be27da12..7d0352c2d4 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -29,7 +29,7 @@ modus-wc-content-tree { min-height: 500px; } - li.modus-wc-tree-item-li-active:not(.modus-wc-tree-dropdown-show li) { + li.modus-wc-tree-item-selected:not(.modus-wc-tree-dropdown-show li) { border-inline-start: 2px solid var(--modus-wc-color-primary); } diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss index 9608ca4494..b35162adb7 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss @@ -8,7 +8,7 @@ modus-wc-tree-item { list-style: none; } - .modus-wc-tree-item-active { + .modus-wc-tree-item-selected > .modus-wc-tree-content { background-color: var(--modus-wc-color-blue-pale); border-radius: unset; color: var(--modus-wc-color-primary); diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts index 42ce9f9d18..5568196238 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts @@ -307,13 +307,13 @@ describe('modus-wc-tree-item', () => { }); const treeItem = page.rootInstance; - expect(treeItem.selected).toBeFalsy(); + expect(treeItem.checked).toBeFalsy(); const checkbox = page.root?.querySelector('modus-wc-checkbox'); checkbox?.dispatchEvent(new MouseEvent('click', { bubbles: true })); await page.waitForChanges(); - expect(treeItem.selected).toBe(true); + expect(treeItem.checked).toBe(true); }); it('checkbox handles Enter key', async () => { @@ -330,7 +330,7 @@ describe('modus-wc-tree-item', () => { ); await page.waitForChanges(); - expect(treeItem.selected).toBe(true); + expect(treeItem.checked).toBe(true); }); it('checkbox handles Space key', async () => { @@ -347,7 +347,7 @@ describe('modus-wc-tree-item', () => { ); await page.waitForChanges(); - expect(treeItem.selected).toBe(true); + expect(treeItem.checked).toBe(true); }); it('checkbox uses correct size - xs converts to sm', async () => { @@ -392,9 +392,9 @@ describe('modus-wc-tree-item', () => { checkbox?.dispatchEvent(new MouseEvent('click', { bubbles: true })); await page.waitForChanges(); - expect(parent.selected).toBe(true); - expect(children[0].selected).toBe(true); - expect(children[1].selected).toBe(true); + expect(parent.checked).toBe(true); + expect(children[0].checked).toBe(true); + expect(children[1].checked).toBe(true); }); it('sets indeterminate state when some children are selected', async () => { @@ -403,7 +403,7 @@ describe('modus-wc-tree-item', () => { html: `
            - +
            @@ -423,7 +423,7 @@ describe('modus-wc-tree-item', () => { await page.waitForChanges(); expect(parent.isIndeterminate).toBe(true); - expect(parent.selected).toBe(false); + expect(parent.checked).toBe(false); }); it('sets selected state when all children are selected', async () => { @@ -432,8 +432,8 @@ describe('modus-wc-tree-item', () => { html: `
            - - + +
            `, @@ -451,7 +451,7 @@ describe('modus-wc-tree-item', () => { await page.waitForChanges(); expect(parent.isIndeterminate).toBe(false); - expect(parent.selected).toBe(true); + expect(parent.checked).toBe(true); }); it('updateIndeterminateState returns early when event target is self', async () => { @@ -595,10 +595,10 @@ describe('modus-wc-tree-item', () => { submenu?.querySelectorAll('modus-wc-tree-item') || [] ) as ITreeItemElement[]; - // First child with checkbox should be selected - expect(children[0]?.selected).toBe(true); - // Second child without checkbox should remain unchanged (not selected) - expect(children[1]?.selected).toBeFalsy(); + // First child with checkbox should be checked + expect(children[0]?.checked).toBe(true); + // Second child without checkbox should remain unchanged (not checked) + expect(children[1]?.checked).toBeFalsy(); }); it('adds event listener on componentDidLoad when has subtree', async () => { @@ -769,7 +769,7 @@ describe('modus-wc-tree-item', () => { checkbox?.dispatchEvent(new MouseEvent('click', { bubbles: true })); await page.waitForChanges(); - expect(treeItem.selected).toBe(true); + expect(treeItem.checked).toBe(true); expect(treeItem.isIndeterminate).toBe(false); }); @@ -815,27 +815,27 @@ describe('modus-wc-tree-item', () => { expect(eventSpy).not.toHaveBeenCalled(); }); - it('handleCheckboxClick sets newValue to true when selected is false', async () => { + it('handleCheckboxClick sets newValue to true when checked is false', async () => { const page = await newSpecPage({ components: [ModusWcTreeItem], html: ``, }); const treeItem = page.rootInstance; - treeItem.selected = false; + treeItem.checked = false; treeItem.isIndeterminate = false; treeItem['handleCheckboxClick'](); await page.waitForChanges(); - expect(treeItem.selected).toBe(true); + expect(treeItem.checked).toBe(true); expect(treeItem.isIndeterminate).toBe(false); }); - it('handleCheckboxClick sets newValue to false when selected is true and not indeterminate', async () => { + it('handleCheckboxClick sets newValue to false when checked is true and not indeterminate', async () => { const page = await newSpecPage({ components: [ModusWcTreeItem], - html: ``, + html: ``, }); const treeItem = page.rootInstance; @@ -844,7 +844,7 @@ describe('modus-wc-tree-item', () => { treeItem['handleCheckboxClick'](); await page.waitForChanges(); - expect(treeItem.selected).toBe(false); + expect(treeItem.checked).toBe(false); expect(treeItem.isIndeterminate).toBe(false); }); @@ -855,20 +855,20 @@ describe('modus-wc-tree-item', () => { }); const treeItem = page.rootInstance; - treeItem.selected = false; + treeItem.checked = false; treeItem.isIndeterminate = true; treeItem['handleCheckboxClick'](); await page.waitForChanges(); - expect(treeItem.selected).toBe(true); + expect(treeItem.checked).toBe(true); expect(treeItem.isIndeterminate).toBe(false); }); - it('handleCheckboxClick sets newValue to true when selected is true but isIndeterminate is also true', async () => { + it('handleCheckboxClick sets newValue to true when checked is true but isIndeterminate is also true', async () => { const page = await newSpecPage({ components: [ModusWcTreeItem], - html: ``, + html: ``, }); const treeItem = page.rootInstance; @@ -878,7 +878,7 @@ describe('modus-wc-tree-item', () => { await page.waitForChanges(); // When indeterminate is true, newValue should be true (OR condition) - expect(treeItem.selected).toBe(true); + expect(treeItem.checked).toBe(true); expect(treeItem.isIndeterminate).toBe(false); }); @@ -889,7 +889,7 @@ describe('modus-wc-tree-item', () => { }); const treeItem = page.rootInstance; - treeItem.selected = false; + treeItem.checked = false; treeItem.isIndeterminate = true; expect(treeItem.isIndeterminate).toBe(true); diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx index fb67608519..e3fbf9b426 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx @@ -16,6 +16,7 @@ import { ITreeItemActions } from '../modus-wc-tree-actions/modus-wc-tree-actions export interface ITreeItemElement extends HTMLElement { value: string; selected?: boolean; + checked?: boolean; checkbox?: boolean; hasSubtree?: boolean; isIndeterminate?: boolean; @@ -52,6 +53,9 @@ export class ModusWcTreeItem { /** The selected state of the tree item. */ @Prop({ mutable: true, reflect: true }) selected?: boolean; + /** The checked state of the tree item when checkbox is enabled. */ + @Prop({ mutable: true, reflect: true }) checked?: boolean; + /** The unique identifying value of the tree item. */ @Prop() value: string = ''; @@ -202,13 +206,13 @@ export class ModusWcTreeItem { const checkboxItems = descendants.filter((item) => item.checkbox); if (!checkboxItems.length) return; - const selectedCount = checkboxItems.filter((item) => item.selected).length; + const checkedCount = checkboxItems.filter((item) => item.checked).length; - const someSelected = selectedCount > 0; - const allSelected = selectedCount === checkboxItems.length; + const someChecked = checkedCount > 0; + const allChecked = checkedCount === checkboxItems.length; - this.selected = allSelected; - this.isIndeterminate = someSelected && !allSelected; + this.checked = allChecked; + this.isIndeterminate = someChecked && !allChecked; }; private updateChildrenSelection = (selected: boolean) => { @@ -224,7 +228,7 @@ export class ModusWcTreeItem { descendants.forEach((item) => { if (!item.checkbox) return; - item.selected = selected; + item.checked = selected; item.isIndeterminate = false; const checkbox = item.querySelector('modus-wc-checkbox'); @@ -240,9 +244,9 @@ export class ModusWcTreeItem { }; private handleCheckboxClick = () => { - const newValue = !this.selected || this.isIndeterminate; + const newValue = !this.checked || this.isIndeterminate; - this.selected = newValue; + this.checked = newValue; this.isIndeterminate = false; this.updateChildrenSelection(newValue); @@ -255,7 +259,7 @@ export class ModusWcTreeItem { rootTreeView.querySelectorAll('modus-wc-tree-item') ) as ITreeItemElement[]; const selectedValues = allTreeItems - .filter((item) => item.checkbox && item.selected) + .filter((item) => item.checkbox && item.checked) .map((item) => item.value); this.selectionsChange.emit({ selectedValues }); @@ -266,7 +270,15 @@ export class ModusWcTreeItem { return (
          • { diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md b/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md index 8db9a72a1d..869f632e34 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/readme.md @@ -14,6 +14,7 @@ A tree item component that represents a single node in a hierarchical tree struc | Property | Attribute | Description | Type | Default | | -------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ----------- | | `checkbox` | `checkbox` | If true, renders a checkbox at the start of the tree item. | `boolean \| undefined` | `false` | +| `checked` | `checked` | The checked state of the tree item when checkbox is enabled. | `boolean \| undefined` | `undefined` | | `customClass` | `custom-class` | Custom CSS class to apply to the li element. | `string \| undefined` | `''` | | `disabled` | `disabled` | The disabled state of the tree item. | `boolean \| undefined` | `undefined` | | `hasSubtree` | `has-subtree` | Whether this tree item has a collapsible subtree. When true, the item will show a caret and handle toggle behavior. | `boolean \| undefined` | `undefined` | diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts index 5c92b9ccec..90ccb1e213 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts @@ -69,69 +69,6 @@ describe('modus-wc-tree-view', () => { expect(ul?.classList.contains('custom-tree')).toBe(true); }); - it('handles itemSelect event and marks item as active', async () => { - const page = await newSpecPage({ - components: [ModusWcTreeView], - html: ` - -
            -
            Item 1
            -
            -
            -
            Item 2
            -
            -
            - `, - }); - - const treeView = page.rootInstance; - const items = page.root?.querySelectorAll('.modus-wc-tree-item'); - const firstItem = items?.[0] as HTMLElement; - const secondItem = items?.[1] as HTMLElement; - - // Simulate item selection on first item - const event = new CustomEvent('itemSelect', { - detail: { value: 'item1' }, - bubbles: true, - }); - Object.defineProperty(event, 'target', { - value: firstItem, - enumerable: true, - }); - - treeView.handleItemSelect(event); - await page.waitForChanges(); - - const firstContent = firstItem.querySelector('.modus-wc-tree-content'); - expect(firstContent?.classList.contains('modus-wc-tree-item-active')).toBe( - true - ); - - // Simulate item selection on second item - const event2 = new CustomEvent('itemSelect', { - detail: { value: 'item2' }, - bubbles: true, - }); - Object.defineProperty(event2, 'target', { - value: secondItem, - enumerable: true, - }); - - treeView.handleItemSelect(event2); - await page.waitForChanges(); - - // First item should no longer be active - expect(firstContent?.classList.contains('modus-wc-tree-item-active')).toBe( - false - ); - - // Second item should be active - const secondContent = secondItem.querySelector('.modus-wc-tree-content'); - expect(secondContent?.classList.contains('modus-wc-tree-item-active')).toBe( - true - ); - }); - it('handles itemSelect event when target is missing', async () => { const page = await newSpecPage({ components: [ModusWcTreeView], @@ -244,48 +181,6 @@ describe('modus-wc-tree-view', () => { expect(classes).toContain('my-custom-sublist'); }); - it('handles itemSelect event and adds class to li element', async () => { - const page = await newSpecPage({ - components: [ModusWcTreeView], - html: ` - - -
          • -
            Item 1
            -
          • - - -
          • -
            Item 2
            -
          • -
            - - `, - }); - - const treeView = page.rootInstance; - const firstTreeItem = page.root?.querySelector( - 'modus-wc-tree-item' - ) as HTMLElement; - const firstLi = firstTreeItem.querySelector('li'); - - const event = new CustomEvent('itemSelect', { - detail: { value: 'item1' }, - bubbles: true, - }); - Object.defineProperty(event, 'target', { - value: firstTreeItem, - enumerable: true, - }); - - treeView.handleItemSelect(event); - await page.waitForChanges(); - - expect(firstLi?.classList.contains('modus-wc-tree-item-li-active')).toBe( - true - ); - }); - it('handles itemSelect event and removes class from previously selected li', async () => { const page = await newSpecPage({ components: [ModusWcTreeView], @@ -310,7 +205,6 @@ describe('modus-wc-tree-view', () => { const firstTreeItem = treeItems?.[0] as HTMLElement; const secondTreeItem = treeItems?.[1] as HTMLElement; const firstLi = firstTreeItem.querySelector('li'); - const secondLi = secondTreeItem.querySelector('li'); // Select first item const event1 = new CustomEvent('itemSelect', { @@ -325,10 +219,6 @@ describe('modus-wc-tree-view', () => { treeView.handleItemSelect(event1); await page.waitForChanges(); - expect(firstLi?.classList.contains('modus-wc-tree-item-li-active')).toBe( - true - ); - // Select second item const event2 = new CustomEvent('itemSelect', { detail: { value: 'item2' }, @@ -342,14 +232,9 @@ describe('modus-wc-tree-view', () => { treeView.handleItemSelect(event2); await page.waitForChanges(); - // First li should no longer have the class expect(firstLi?.classList.contains('modus-wc-tree-item-li-active')).toBe( false ); - // Second li should have the class - expect(secondLi?.classList.contains('modus-wc-tree-item-li-active')).toBe( - true - ); }); it('handles itemSelect event when content element has no parent li', async () => { diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx index b905d9743d..b12530783e 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx @@ -31,24 +31,14 @@ export class ModusWcTreeView { const target = event.target as HTMLElement; if (!target) return; - console.log('target', target); + const allTreeItems = this.el.querySelectorAll('modus-wc-tree-item'); + allTreeItems.forEach((item: HTMLModusWcTreeItemElement) => { + item.selected = false; + }); - const allContents = this.el.querySelectorAll('.modus-wc-tree-content'); - allContents.forEach((content) => - content.classList.remove('modus-wc-tree-item-active') - ); - - const targetContent = target.querySelector('.modus-wc-tree-content'); - targetContent?.classList.add('modus-wc-tree-item-active'); - - const allTreeItemLis = this.el.querySelectorAll('modus-wc-tree-item > li'); - allTreeItemLis.forEach((li) => - li.classList.remove('modus-wc-tree-item-li-active') - ); - - const targetLi = targetContent?.parentElement; - if (targetLi?.tagName === 'LI') { - targetLi.classList.add('modus-wc-tree-item-li-active'); + const targetTreeItem = target.closest('modus-wc-tree-item'); + if (targetTreeItem) { + (targetTreeItem as HTMLModusWcTreeItemElement).selected = true; } } diff --git a/src/custom-elements.json b/src/custom-elements.json index 3737a0804d..eb0bfe7d9c 100644 --- a/src/custom-elements.json +++ b/src/custom-elements.json @@ -2552,6 +2552,14 @@ "text": "boolean" } }, + { + "name": "checked", + "fieldName": "checked", + "description": "The checked state of the tree item when checkbox is enabled.", + "type": { + "text": "boolean" + } + }, { "name": "custom-class", "fieldName": "customClass", From a64f8168d4564da74ba8fb15f41b68cccbf7372e Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:39:07 +0530 Subject: [PATCH 28/39] 662 - fix pr comments --- src/components.d.ts | 4 +- .../modus-wc-content-tree.scss | 1 - .../modus-wc-content-tree.stories.ts | 310 +++++++++++++----- .../modus-wc-tree-actions.tsx | 3 +- .../modus-wc-tree-item.scss | 7 +- .../modus-wc-tree-item/modus-wc-tree-item.tsx | 10 +- .../modus-wc-tree-view/modus-wc-tree-view.tsx | 5 +- 7 files changed, 253 insertions(+), 87 deletions(-) diff --git a/src/components.d.ts b/src/components.d.ts index 69b0bce1b4..f7e796ca4e 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -2077,7 +2077,7 @@ export namespace Components { /** * The size of the tree item icons and actions. */ - "size": 'xs' | 'sm' | 'md' | 'lg'; + "size": DaisySize; /** * Actions to display for this tree item. */ @@ -5670,7 +5670,7 @@ declare namespace LocalJSX { /** * The size of the tree item icons and actions. */ - "size"?: 'xs' | 'sm' | 'md' | 'lg'; + "size"?: DaisySize; /** * Actions to display for this tree item. */ diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index 7d0352c2d4..d57376b3eb 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -25,7 +25,6 @@ modus-wc-content-tree { } .modus-wc-content-tree-content { - align-items: center; min-height: 500px; } diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts index 99e1ffd693..8757d80b13 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts @@ -1,7 +1,7 @@ import { withActions } from '@storybook/addon-actions/decorator'; import { Meta, StoryObj } from '@storybook/web-components'; import { html } from 'lit'; -// import { ifDefined } from 'lit/directives/if-defined.js'; +import { ITreeItemElement } from './modus-wc-tree-item/modus-wc-tree-item'; interface ContentTreeArgs { 'custom-class'?: string; @@ -42,6 +42,39 @@ export default meta; type Story = StoryObj; export const Default: Story = { + parameters: { + docs: { + description: { + story: + 'A basic content tree with hierarchical structure. Items can be expanded and collapsed to navigate through the tree.', + }, + source: { + code: ` + + + + + + + + + + + + + + + + + + + + + +`, + }, + }, + }, render: (args) => { return html` + +`, + }, + }, + }, render: (args) => { return html` + + + + + + + + + + + + + +`, + }, + }, + }, render: (args) => { return html` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`, + }, + }, + }, render: (args) => { return html` { - const actions = [ - { - id: 'view', - label: 'View', - icon: 'visibility_on', - ariaLabel: 'View item', - }, - { - id: 'add', - label: 'Add new above', - icon: 'add', - ariaLabel: 'Add new item', + name: 'With Actions', + parameters: { + docs: { + description: { + story: + 'This example demonstrates tree items with custom actions. Actions can be used to perform operations like toggling visibility or deleting items.', }, + source: { + code: ` + + + + + + + + + + + +`, }, + }, + }, + render: (args) => { + const getTreeItemActions = (isDisabled: boolean) => [ { - id: 'make-copy', - label: 'Make a copy', - icon: 'copy_content', - ariaLabel: 'Make a copy of item', + id: 'toggle-visibility', + label: isDisabled ? 'Hidden' : 'Visible', + icon: isDisabled ? 'visibility_off' : 'visibility_on', + ariaLabel: isDisabled ? 'Set item to visible' : 'Set item to hidden', + size: 'sm', }, { id: 'delete', label: 'Delete', icon: 'delete', ariaLabel: 'Delete item', + size: 'sm', }, ]; + const handleTreeActionClick = ( + event: CustomEvent<{ actionId: string }> + ) => { + const actionSource = event.target as HTMLElement; + const treeItem = actionSource.closest( + 'modus-wc-tree-item' + ) as ITreeItemElement; + + if (!treeItem) return; + + if (event.detail.actionId === 'delete') { + treeItem.remove(); + return; + } + + if (event.detail.actionId !== 'toggle-visibility') return; + + treeItem.disabled = !treeItem.disabled; + treeItem.treeItemActions = getTreeItemActions(treeItem.disabled); + }; + return html` - - - - - - - - - - - - - - - - diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx index 601847057d..0964156ab1 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx @@ -172,7 +172,6 @@ export class ModusWcTreeActions { key={action.id} customClass="modus-wc-tree-action-button" disabled={action.disabled} - variant="borderless" size={this.size} shape="circle" onClick={(e) => this.handleActionClick(action, e)} @@ -188,13 +187,13 @@ export class ModusWcTreeActions {
            (this.moreActionsButton = el as HTMLElement)} onClick={this.handleMoreActionsClick} aria-expanded={this.isDropdownOpen ? 'true' : 'false'} aria-haspopup="true" + aria-label="More actions" > ; expandSubTree(): Promise; } @@ -45,7 +51,7 @@ export class ModusWcTreeItem { @Prop() checkbox?: boolean = false; /** The text label displayed for the tree item. */ - @Prop() label!: string; + @Prop({ reflect: true }) label!: string; /** Custom CSS class to apply to the li element. */ @Prop() customClass?: string = ''; @@ -66,7 +72,7 @@ export class ModusWcTreeItem { @Prop() treeItemActions?: ITreeItemActions[]; /** The size of the tree item icons and actions. */ - @Prop() size: 'xs' | 'sm' | 'md' | 'lg' = 'xs'; + @Prop() size: DaisySize = 'xs'; /** Internal state to track if subtree is expanded */ @State() isExpanded: boolean = false; diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx index b12530783e..d5fb8b4056 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx @@ -1,5 +1,6 @@ import { Component, Element, h, Host, Listen, Prop } from '@stencil/core'; import { Attributes, inheritAriaAttributes } from '../../utils'; +import { ITreeItemElement } from '../modus-wc-tree-item/modus-wc-tree-item'; /** * A wrapper component that provides the ul element for tree items. @@ -32,13 +33,13 @@ export class ModusWcTreeView { if (!target) return; const allTreeItems = this.el.querySelectorAll('modus-wc-tree-item'); - allTreeItems.forEach((item: HTMLModusWcTreeItemElement) => { + allTreeItems.forEach((item: ITreeItemElement) => { item.selected = false; }); const targetTreeItem = target.closest('modus-wc-tree-item'); if (targetTreeItem) { - (targetTreeItem as HTMLModusWcTreeItemElement).selected = true; + (targetTreeItem as ITreeItemElement).selected = true; } } From f823a60aa23df284caa26ca14861c674ca76764e Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:59:48 +0530 Subject: [PATCH 29/39] 662 - address pr comments --- src/components.d.ts | 4 +- .../modus-wc-content-tree.stories.ts | 138 +++++++++++++++++- .../modus-wc-tree-actions.scss | 2 +- .../modus-wc-tree-actions.tsx | 28 +++- .../modus-wc-tree-item.spec.ts.snap | 6 +- .../modus-wc-tree-item.tailwind.ts | 4 +- .../modus-wc-tree-item/modus-wc-tree-item.tsx | 12 +- src/custom-elements.json | 4 +- 8 files changed, 181 insertions(+), 17 deletions(-) diff --git a/src/components.d.ts b/src/components.d.ts index f7e796ca4e..184c58ef71 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -2032,7 +2032,7 @@ export namespace Components { /** * The size of the action buttons and icons. */ - "size": 'xs' | 'sm' | 'md' | 'lg'; + "size": DaisySize; } /** * A tree item component that represents a single node in a hierarchical tree structure. @@ -5621,7 +5621,7 @@ declare namespace LocalJSX { /** * The size of the action buttons and icons. */ - "size"?: 'xs' | 'sm' | 'md' | 'lg'; + "size"?: DaisySize; } /** * A tree item component that represents a single node in a hierarchical tree structure. diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts index 8757d80b13..6cbf1b6c53 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts @@ -1,6 +1,8 @@ import { withActions } from '@storybook/addon-actions/decorator'; import { Meta, StoryObj } from '@storybook/web-components'; import { html } from 'lit'; +import { DaisySize } from '../types'; +import { ITreeItemActions } from './modus-wc-tree-actions/modus-wc-tree-actions'; import { ITreeItemElement } from './modus-wc-tree-item/modus-wc-tree-item'; interface ContentTreeArgs { @@ -8,6 +10,17 @@ interface ContentTreeArgs { 'search-placeholder'?: string; 'include-search'?: boolean; 'include-actions'?: boolean; + // Tree Item Props + disabled?: boolean; + checkbox?: boolean; + label?: string; + 'custom-class-item'?: string; + selected?: boolean; + checked?: boolean; + value?: string; + 'has-subtree'?: boolean; + 'tree-item-actions'?: ITreeItemActions[]; + size?: DaisySize; } const meta: Meta = { @@ -17,16 +30,64 @@ const meta: Meta = { 'search-placeholder': 'Search...', 'include-search': true, 'include-actions': true, + disabled: false, + checkbox: false, + label: 'Tree Item', + selected: false, + checked: false, + value: '', + 'has-subtree': false, + size: 'xs', }, argTypes: { 'search-placeholder': { control: { type: 'text' }, + table: { category: 'Content Tree' }, }, 'include-search': { control: { type: 'boolean' }, + table: { category: 'Content Tree' }, }, 'include-actions': { control: { type: 'boolean' }, + table: { category: 'Content Tree' }, + }, + disabled: { + control: { type: 'boolean' }, + table: { category: 'Tree Item' }, + }, + checkbox: { + control: { type: 'boolean' }, + table: { category: 'Tree Item' }, + }, + label: { + control: { type: 'text' }, + table: { category: 'Tree Item' }, + }, + 'custom-class-item': { + control: { type: 'text' }, + table: { category: 'Tree Item' }, + }, + selected: { + control: { type: 'boolean' }, + table: { category: 'Tree Item' }, + }, + checked: { + control: { type: 'boolean' }, + table: { category: 'Tree Item' }, + }, + value: { + control: { type: 'text' }, + table: { category: 'Tree Item' }, + }, + 'has-subtree': { + control: { type: 'boolean' }, + table: { category: 'Tree Item' }, + }, + size: { + control: { type: 'select' }, + options: ['xs', 'sm', 'md', 'lg'], + table: { category: 'Tree Item' }, }, }, decorators: [withActions], @@ -124,6 +185,79 @@ export const Default: Story = { }, }; +export const TreeItem: Story = { + name: 'Tree Item', + parameters: { + docs: { + description: { + story: + 'A single tree item demonstrating all available properties. Use the controls to customize the tree item appearance and behavior.', + }, + source: { + code: ` + + + + + + + + + +`, + }, + }, + }, + render: (args) => { + const defaultTreeItemActions: ITreeItemActions[] = [ + { + id: 'info', + label: 'Info', + icon: 'info', + ariaLabel: 'Info item', + }, + ]; + + return html` + + + + `; + }, +}; + export const EmptyState: Story = { name: 'Empty State', parameters: { @@ -229,8 +363,8 @@ export const DisabledItems: Story = { }, }; -export const MultiSelect: Story = { - name: 'Multi Select', +export const CheckboxSelection: Story = { + name: 'Checkbox Selection', parameters: { docs: { description: { diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss index 8501e8c367..f2bfe7db70 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss @@ -29,7 +29,7 @@ modus-wc-tree-actions { display: none; min-width: 150px; padding: 4px 0; - position: fixed; + position: absolute; z-index: 1000; &.show { diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx index 0964156ab1..4e727f5867 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.tsx @@ -11,6 +11,7 @@ import { Event as StencilEvent, Watch, } from '@stencil/core'; +import { DaisySize } from '../../types'; export interface ITreeItemActions { id: string; // Unique identifier for the action @@ -43,7 +44,7 @@ export class ModusWcTreeActions { @Prop() actions?: ITreeItemActions[]; /** The size of the action buttons and icons. */ - @Prop() size: 'xs' | 'sm' | 'md' | 'lg' = 'xs'; + @Prop() size: DaisySize = 'xs'; /** Internal state for dropdown visibility */ @State() isDropdownOpen: boolean = false; @@ -142,7 +143,7 @@ export class ModusWcTreeActions { this.moreActionsDropdown, { placement: 'bottom', - strategy: 'fixed', + strategy: 'absolute', modifiers: [ { name: 'offset', @@ -150,10 +151,33 @@ export class ModusWcTreeActions { offset: [0, 8], }, }, + { + name: 'preventOverflow', + options: { + padding: 8, + boundary: 'viewport', + }, + }, { name: 'flip', options: { fallbackPlacements: ['top-start', 'bottom-end', 'top-end'], + padding: 8, + boundary: 'viewport', + }, + }, + { + name: 'computeStyles', + options: { + adaptive: true, + gpuAcceleration: true, + }, + }, + { + name: 'eventListeners', + options: { + scroll: true, + resize: true, }, }, ], diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/__snapshots__/modus-wc-tree-item.spec.ts.snap b/src/components/modus-wc-content-tree/modus-wc-tree-item/__snapshots__/modus-wc-tree-item.spec.ts.snap index 9e1ca7fd3b..2b4f704647 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/__snapshots__/modus-wc-tree-item.spec.ts.snap +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/__snapshots__/modus-wc-tree-item.spec.ts.snap @@ -5,10 +5,10 @@ exports[`modus-wc-tree-item renders with checkbox 1`] = `
          • - + - +
            Test Item @@ -27,7 +27,7 @@ exports[`modus-wc-tree-item renders with default props 1`] = `
          • - +
            diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tailwind.ts b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tailwind.ts index 25e66b58e7..66ae18a324 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tailwind.ts +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tailwind.ts @@ -1,3 +1,5 @@ +import { DaisySize } from '../../types'; + export const convertPropsToClasses = ({ disabled, selected, @@ -5,7 +7,7 @@ export const convertPropsToClasses = ({ }: { disabled?: boolean; selected?: boolean; - size?: 'xs' | 'sm' | 'md' | 'lg'; + size?: DaisySize; }): string => { let classes = ''; diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx index 629ea8cfd5..dd91f087ae 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx @@ -10,9 +10,9 @@ import { Event as StencilEvent, } from '@stencil/core'; import { convertPropsToClasses } from './modus-wc-tree-item.tailwind'; +import { DaisySize } from '../../types'; import { Attributes, inheritAriaAttributes } from '../../utils'; import { ITreeItemActions } from '../modus-wc-tree-actions/modus-wc-tree-actions'; -import { DaisySize } from '../../types'; export interface ITreeItemElement extends HTMLElement { value: string; @@ -25,7 +25,7 @@ export interface ITreeItemElement extends HTMLElement { label: string; customClass?: string; treeItemActions?: ITreeItemActions[]; - size?: 'xs' | 'sm' | 'md' | 'lg'; + size?: DaisySize; collapseSubTree(): Promise; expandSubTree(): Promise; } @@ -299,7 +299,11 @@ export class ModusWcTreeItem { shape="circle" size={this.size} customClass="modus-wc-tree-toggle-btn" - aria-label={this.isExpanded ? 'Collapse' : 'Expand'} + aria-label={ + this.isExpanded + ? `Collapse ${this.label}` + : `Expand ${this.label}` + } disabled={!this.hasSubtree} onButtonClick={(e) => { this.handleToggleClick(e.detail); @@ -313,7 +317,7 @@ export class ModusWcTreeItem { {this.checkbox && ( Date: Tue, 3 Mar 2026 12:51:06 +0530 Subject: [PATCH 30/39] 662 - update storybook --- .../modus-wc-content-tree.stories.ts | 235 ++++++++---------- .../modus-wc-tree-view.spec.ts | 37 +++ .../modus-wc-tree-view/modus-wc-tree-view.tsx | 2 + 3 files changed, 143 insertions(+), 131 deletions(-) diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts index 6cbf1b6c53..2015864ce9 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts @@ -1,8 +1,6 @@ import { withActions } from '@storybook/addon-actions/decorator'; import { Meta, StoryObj } from '@storybook/web-components'; import { html } from 'lit'; -import { DaisySize } from '../types'; -import { ITreeItemActions } from './modus-wc-tree-actions/modus-wc-tree-actions'; import { ITreeItemElement } from './modus-wc-tree-item/modus-wc-tree-item'; interface ContentTreeArgs { @@ -10,34 +8,16 @@ interface ContentTreeArgs { 'search-placeholder'?: string; 'include-search'?: boolean; 'include-actions'?: boolean; - // Tree Item Props - disabled?: boolean; - checkbox?: boolean; - label?: string; - 'custom-class-item'?: string; - selected?: boolean; - checked?: boolean; - value?: string; - 'has-subtree'?: boolean; - 'tree-item-actions'?: ITreeItemActions[]; - size?: DaisySize; } const meta: Meta = { title: 'Components/Content Tree', component: 'modus-wc-content-tree', args: { + 'custom-class': '', 'search-placeholder': 'Search...', 'include-search': true, 'include-actions': true, - disabled: false, - checkbox: false, - label: 'Tree Item', - selected: false, - checked: false, - value: '', - 'has-subtree': false, - size: 'xs', }, argTypes: { 'search-placeholder': { @@ -52,43 +32,6 @@ const meta: Meta = { control: { type: 'boolean' }, table: { category: 'Content Tree' }, }, - disabled: { - control: { type: 'boolean' }, - table: { category: 'Tree Item' }, - }, - checkbox: { - control: { type: 'boolean' }, - table: { category: 'Tree Item' }, - }, - label: { - control: { type: 'text' }, - table: { category: 'Tree Item' }, - }, - 'custom-class-item': { - control: { type: 'text' }, - table: { category: 'Tree Item' }, - }, - selected: { - control: { type: 'boolean' }, - table: { category: 'Tree Item' }, - }, - checked: { - control: { type: 'boolean' }, - table: { category: 'Tree Item' }, - }, - value: { - control: { type: 'text' }, - table: { category: 'Tree Item' }, - }, - 'has-subtree': { - control: { type: 'boolean' }, - table: { category: 'Tree Item' }, - }, - size: { - control: { type: 'select' }, - options: ['xs', 'sm', 'md', 'lg'], - table: { category: 'Tree Item' }, - }, }, decorators: [withActions], parameters: { @@ -185,79 +128,6 @@ export const Default: Story = { }, }; -export const TreeItem: Story = { - name: 'Tree Item', - parameters: { - docs: { - description: { - story: - 'A single tree item demonstrating all available properties. Use the controls to customize the tree item appearance and behavior.', - }, - source: { - code: ` - - - - - - - - - -`, - }, - }, - }, - render: (args) => { - const defaultTreeItemActions: ITreeItemActions[] = [ - { - id: 'info', - label: 'Info', - icon: 'info', - ariaLabel: 'Info item', - }, - ]; - - return html` - - - - `; - }, -}; - export const EmptyState: Story = { name: 'Empty State', parameters: { @@ -717,3 +587,106 @@ contentTree.addEventListener('treeActionClick', handleTreeActionClick); `; }, }; + +export const ApiReference: Story = { + name: 'API Reference', + parameters: { + docs: { + description: { + story: ` +### Props + +| Name | Type | Default | Description | +|-------------------|------------|--------------|---------------------------------------------------| +| customClass | \`string\` | \`''\` | Additional CSS class to apply to the component | +| searchPlaceholder | \`string\` | \`'Search...'\` | Placeholder text for the search input | +| includeSearch | \`boolean\` | \`true\` | Whether to display the search functionality | +| includeActions | \`boolean\` | \`true\` | Whether to display action buttons for tree items | + +--- + +### Tree View + +#### Props + +| Name | Type | Default | Description | +|-------------|------------|-----------|-------------------------------------------------------| +| customClass | \`string\` | \`''\` | Additional CSS class to apply to the tree view | +| isSubList | \`boolean\` | \`false\` | Whether the tree view is a sublist of another tree item | + +--- + +### Tree Item + +#### Props + +| Name | Type | Default | Description | +|-----------------|----------------------------------------|-----------|--------------------------------------------------------------| +| label | \`string\` | - | The label text for the tree item (required) | +| value | \`string\` | \`''\` | The value associated with the tree item | +| disabled | \`boolean\` | \`false\` | Whether the tree item is disabled | +| checkbox | \`boolean\` | \`false\` | Whether to display a checkbox for the tree item | +| selected | \`boolean\` | - | Whether the tree item is selected (mutable, reflected) | +| checked | \`boolean\` | - | Whether the tree item checkbox is checked (mutable, reflected) | +| hasSubtree | \`boolean\` | \`false\` | Whether the tree item has a subtree | +| treeItemActions | \`ITreeItemActions[]\` | - | Array of actions to display for the tree item | +| size | \`'xs' | 'sm' | 'md' | 'lg'\` | \`'xs'\` | The size of the tree item | +| customClass | \`string\` | \`''\` | Additional CSS class to apply to the tree item | + +#### Events + +| Name | Payload | Description | +|------------------|----------------------------------|-------------------------------------------------| +| itemSelect | \`{ value: string }\` | Emitted when a tree item is selected | +| selectionsChange | \`{ selectedValues: string[] }\` | Emitted when the selection state changes | + +#### Methods + +| Name | Type | Description | +|-----------------|---------------------------|----------------------------| +| collapseSubTree | \`() => Promise\` | Collapses the subtree | +| expandSubTree | \`() => Promise\` | Expands the subtree | + +--- + +### Tree Actions + +#### Props + +| Name | Type | Default | Description | +|---------|-------------------------------------|----------|--------------------------------------| +| actions | \`ITreeItemActions[]\` | - | Array of actions to display | +| size | \`'xs' | 'sm' | 'md' | 'lg'\` | \`'xs'\` | The size of the action buttons | + +#### Events + +| Name | Payload | Description | +|-----------------|-------------------------------------------|--------------------------------------------| +| treeActionClick | \`{ actionId: string; actionName: string }\` | Emitted when an action is clicked | +| dropdownOpened | \`HTMLElement\` | Emitted when the dropdown is opened | + +--- + +### Interfaces + +#### ITreeItemActions + +\`\`\`typescript +interface ITreeItemActions { + id: string; // Unique identifier for the action + icon: string; // Icon name for the action, e.g., 'edit', 'trash' + iconVariant?: 'solid' | 'outlined'; // Optional variant for the icon + label: string; // Text label for the action + ariaLabel?: string; // Optional label for accessibility + disabled?: boolean; // Optional flag to disable the action +} +\`\`\` +`, + }, + }, + controls: { disable: true }, + }, + render: () => { + return html``; + }, +}; diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts index 90ccb1e213..aadcedf1ad 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.spec.ts @@ -1,5 +1,6 @@ import { newSpecPage } from '@stencil/core/testing'; import { ModusWcTreeView } from './modus-wc-tree-view'; +import { ITreeItemElement } from '../modus-wc-tree-item/modus-wc-tree-item'; describe('modus-wc-tree-view', () => { it('renders with default props', async () => { @@ -117,6 +118,42 @@ describe('modus-wc-tree-view', () => { expect(() => treeView.handleItemSelect(event)).not.toThrow(); }); + it('does not handle itemSelect when isSubList is true', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeView], + html: ` + + +
          • +
            Item 1
            +
          • + + + `, + }); + + const treeView = page.rootInstance; + const treeItem = page.root?.querySelector( + 'modus-wc-tree-item' + ) as HTMLElement; + + const event = new CustomEvent('itemSelect', { + detail: { value: 'item1' }, + bubbles: true, + }); + Object.defineProperty(event, 'target', { + value: treeItem, + enumerable: true, + }); + + treeView.handleItemSelect(event); + await page.waitForChanges(); + + // When isSubList is true, the event should be ignored and item should not be selected + const treeItemElement = treeItem as ITreeItemElement; + expect(treeItemElement.selected).toBeFalsy(); + }); + it('inherits ARIA attributes', async () => { const page = await newSpecPage({ components: [ModusWcTreeView], diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx index d5fb8b4056..ebf857e167 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-view/modus-wc-tree-view.tsx @@ -29,6 +29,8 @@ export class ModusWcTreeView { @Listen('itemSelect') handleItemSelect(event: CustomEvent<{ value: string }>) { + if (this.isSubList) return; + const target = event.target as HTMLElement; if (!target) return; From 4cbd0c67b44783dda862d23c092a74d1fb8bfb46 Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:58:23 +0530 Subject: [PATCH 31/39] 662 - update storybook --- .../modus-wc-content-tree.scss | 12 - .../modus-wc-content-tree.stories.ts | 243 ++++++++++++++++-- .../modus-wc-tree-item.scss | 22 ++ .../modus-wc-tree-item.spec.ts | 107 ++++++++ .../modus-wc-tree-item/modus-wc-tree-item.tsx | 14 +- 5 files changed, 355 insertions(+), 43 deletions(-) diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index d57376b3eb..ae7813bbe0 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -56,15 +56,3 @@ modus-wc-content-tree { } } } - -[data-theme='modus-classic-dark'], -[data-theme='modus-modern-dark'], -[data-theme='connect-dark'] { - modus-wc-content-tree { - .modus-wc-content-tree-actions { - .modus-wc-content-tree-action-icon { - color: var(--modus-wc-color-gray-light); - } - } - } -} diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts index 2015864ce9..29897ff4dc 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.stories.ts @@ -36,7 +36,12 @@ const meta: Meta = { decorators: [withActions], parameters: { actions: { - handles: ['itemSelect', 'treeActionClick', 'selectionsChange'], + handles: [ + 'itemSelect', + 'treeActionClick', + 'selectionsChange', + 'dropdownOpened', + ], }, }, }; @@ -128,6 +133,114 @@ export const Default: Story = { }, }; +export const TreeItem: Story = { + name: 'Tree Item', + parameters: { + docs: { + description: { + story: + 'A comprehensive example showing tree item features: checkbox, start icon, and actions.', + }, + source: { + code: ` + + + + + +`, + }, + }, + }, + render: () => { + const actions = [ + { id: 'edit', icon: 'pencil', label: 'Edit' }, + { id: 'delete', icon: 'trash', label: 'Delete' }, + ]; + return html` + + + + + + `; + }, +}; + +export const TreeItemWithStartIcon: Story = { + name: 'Tree Item - With Start Icon', + parameters: { + docs: { + description: { + story: + 'Tree items can display custom icons at the start using the start-icon slot. This is useful for representing file types, folders, or custom item types.', + }, + source: { + code: ` + + + + + + + + + + + + + + + + + + + +`, + }, + }, + }, + render: () => { + return html` + + + + + + + + + + + + + + + + + + + + `; + }, +}; + export const EmptyState: Story = { name: 'Empty State', parameters: { @@ -157,28 +270,35 @@ export const EmptyState: Story = { }, }; -export const DisabledItems: Story = { - name: 'Disabled Items', +export const SingleSelection: Story = { + name: 'Single Selection', parameters: { docs: { description: { story: - 'This example demonstrates tree items with disabled state. Disabled items cannot be selected or interacted with.', + 'Content tree with single selection mode. Click on any tree item to select it. Only one item can be selected at a time.', }, source: { code: ` - - + + + + + - - - + + + + + + + `, @@ -195,38 +315,39 @@ export const DisabledItems: Story = { > - - - + + + + + + - - + + + + + + + + `; @@ -449,6 +570,72 @@ export const CheckboxSelection: Story = { `; }, }; + +export const DisabledSelection: Story = { + name: 'Disabled Selection', + parameters: { + docs: { + description: { + story: + 'This example demonstrates tree items with disabled state. Disabled items cannot be selected or interacted with.', + }, + source: { + code: ` + + + + + + + + + + + + +`, + }, + }, + }, + render: (args) => { + return html` + + + + + + + + + + + + + + + + + `; + }, +}; + export const WithActions: Story = { name: 'With Actions', parameters: { diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss index fcb3f26ba4..9e498a1172 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss @@ -64,6 +64,28 @@ modus-wc-tree-item { margin-inline-start: auto; } + .modus-wc-tree-toggle-spacer { + display: inline-block; + flex-shrink: 0; + height: 1.5rem; + width: 1.5rem; + } + + .modus-wc-tree-item-sm .modus-wc-tree-toggle-spacer { + height: 1.75rem; + width: 1.75rem; + } + + .modus-wc-tree-item-md .modus-wc-tree-toggle-spacer { + height: 2rem; + width: 2rem; + } + + .modus-wc-tree-item-lg .modus-wc-tree-toggle-spacer { + height: 2.25rem; + width: 2.25rem; + } + button { align-items: center; background: transparent; diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts index 5568196238..0a315e1d0b 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.spec.ts @@ -799,6 +799,30 @@ describe('modus-wc-tree-item', () => { document.body.removeChild(contentTree); }); + it('emits selectionsChange with selected values inside standalone tree view', async () => { + const treeView = document.createElement('modus-wc-tree-view'); + + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + treeView.appendChild(page.root as HTMLElement); + document.body.appendChild(treeView); + + const eventSpy = jest.fn(); + page.root?.addEventListener('selectionsChange', eventSpy); + + const treeItem = page.rootInstance; + treeItem['handleCheckboxClick'](); + await page.waitForChanges(); + + expect(eventSpy).toHaveBeenCalled(); + expect(eventSpy.mock.calls[0][0].detail.selectedValues).toEqual(['item1']); + + document.body.removeChild(treeView); + }); + it('does not emit selectionsChange when rootTreeView is not found', async () => { const page = await newSpecPage({ components: [ModusWcTreeItem], @@ -899,4 +923,87 @@ describe('modus-wc-tree-item', () => { expect(treeItem.isIndeterminate).toBe(false); }); + + it('getRootTreeView returns the closest tree-view when no parent tree-views exist', async () => { + const treeView = document.createElement('modus-wc-tree-view'); + + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + treeView.appendChild(page.root as HTMLElement); + document.body.appendChild(treeView); + + const treeItem = page.rootInstance; + const result = treeItem['getRootTreeView'](); + + expect(result).toBe(treeView); + + document.body.removeChild(treeView); + }); + + it('getRootTreeView traverses up to find root tree-view in nested structure', async () => { + const rootTreeView = document.createElement('modus-wc-tree-view'); + const parentItem = document.createElement('modus-wc-tree-item'); + const nestedTreeView = document.createElement('modus-wc-tree-view'); + + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + // Structure: rootTreeView > parentItem > nestedTreeView > childItem + nestedTreeView.appendChild(page.root as HTMLElement); + parentItem.appendChild(nestedTreeView); + rootTreeView.appendChild(parentItem); + document.body.appendChild(rootTreeView); + + const treeItem = page.rootInstance; + const result = treeItem['getRootTreeView'](); + + expect(result).toBe(rootTreeView); + + document.body.removeChild(rootTreeView); + }); + + it('getRootTreeView traverses multiple levels to find root tree-view', async () => { + const rootTreeView = document.createElement('modus-wc-tree-view'); + const level1Item = document.createElement('modus-wc-tree-item'); + const level2TreeView = document.createElement('modus-wc-tree-view'); + const level2Item = document.createElement('modus-wc-tree-item'); + const level3TreeView = document.createElement('modus-wc-tree-view'); + + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + // Structure: rootTreeView > level1Item > level2TreeView > level2Item > level3TreeView > deepChild + level3TreeView.appendChild(page.root as HTMLElement); + level2Item.appendChild(level3TreeView); + level2TreeView.appendChild(level2Item); + level1Item.appendChild(level2TreeView); + rootTreeView.appendChild(level1Item); + document.body.appendChild(rootTreeView); + + const treeItem = page.rootInstance; + const result = treeItem['getRootTreeView'](); + + expect(result).toBe(rootTreeView); + + document.body.removeChild(rootTreeView); + }); + + it('getRootTreeView returns null when not inside any tree-view', async () => { + const page = await newSpecPage({ + components: [ModusWcTreeItem], + html: ``, + }); + + const treeItem = page.rootInstance; + const result = treeItem['getRootTreeView'](); + + expect(result).toBeNull(); + }); }); diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx index dd91f087ae..7b6bd49550 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.tsx @@ -249,6 +249,16 @@ export class ModusWcTreeItem { this.itemSelect.emit({ value: this.value }); }; + private getRootTreeView(): HTMLElement | null { + let current: HTMLElement | null = this.el.closest('modus-wc-tree-view'); + + while (current?.parentElement?.closest('modus-wc-tree-view')) { + current = current.parentElement.closest('modus-wc-tree-view'); + } + + return current; + } + private handleCheckboxClick = () => { const newValue = !this.checked || this.isIndeterminate; @@ -257,9 +267,7 @@ export class ModusWcTreeItem { this.updateChildrenSelection(newValue); // Emit selectionChange event with all selected values for multi-select mode - const rootTreeView = this.el - .closest('modus-wc-content-tree') - ?.querySelector('modus-wc-tree-view'); + const rootTreeView = this.getRootTreeView(); if (rootTreeView) { const allTreeItems = Array.from( rootTreeView.querySelectorAll('modus-wc-tree-item') From f817936977f83ade27e8fcffc348ce62932f8d06 Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:23:08 +0530 Subject: [PATCH 32/39] 662 - dark mode fixes --- src/components.d.ts | 31 +++++++++++++- .../modus-wc-content-tree.scss | 12 ++++++ .../modus-wc-tree-item.scss | 14 +++---- src/components/modus-wc-logo/README.md | 13 ++++++ src/components/modus-wc-navbar/readme.md | 41 ++++++++++++------- src/custom-elements.json | 22 +++++++++- 6 files changed, 107 insertions(+), 26 deletions(-) diff --git a/src/components.d.ts b/src/components.d.ts index 184c58ef71..dc7a360e34 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -1069,6 +1069,10 @@ export namespace Components { /** * A customizable navbar component used for top level navigation of all Trimble applications. * The component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML. + * ⚠️ Deprecation Alert + * The `trimbleLogoClick` event is deprecated and will be removed in a future major version. + * Please use the `logoClick` event instead, which serves the same purpose and is not tied to a specific logo name. + * The `logoClick` event will be emitted whenever the logo is clicked, regardless of the `logoName` prop value. */ interface ModusWcNavbar { /** @@ -1087,6 +1091,10 @@ export namespace Components { * Custom CSS class to apply to the host element. */ "customClass"?: string; + /** + * The name of the logo to display. Supports any valid 'logo-name' from the 'modus-wc-logo' component. Defaults to 'trimble'. + */ + "logoName"?: LogoName; /** * The open state of the main menu. */ @@ -2728,12 +2736,17 @@ declare global { "searchClick": MouseEvent | KeyboardEvent; "searchInputOpenChange": boolean; "signOutClick": MouseEvent | KeyboardEvent; + "logoClick": MouseEvent | KeyboardEvent; "trimbleLogoClick": MouseEvent | KeyboardEvent; "userMenuOpenChange": boolean; } /** * A customizable navbar component used for top level navigation of all Trimble applications. * The component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML. + * ⚠️ Deprecation Alert + * The `trimbleLogoClick` event is deprecated and will be removed in a future major version. + * Please use the `logoClick` event instead, which serves the same purpose and is not tied to a specific logo name. + * The `logoClick` event will be emitted whenever the logo is clicked, regardless of the `logoName` prop value. */ interface HTMLModusWcNavbarElement extends Components.ModusWcNavbar, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLModusWcNavbarElement, ev: ModusWcNavbarCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -4422,6 +4435,10 @@ declare namespace LocalJSX { /** * A customizable navbar component used for top level navigation of all Trimble applications. * The component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML. + * ⚠️ Deprecation Alert + * The `trimbleLogoClick` event is deprecated and will be removed in a future major version. + * Please use the `logoClick` event instead, which serves the same purpose and is not tied to a specific logo name. + * The `logoClick` event will be emitted whenever the logo is clicked, regardless of the `logoName` prop value. */ interface ModusWcNavbar { /** @@ -4440,6 +4457,10 @@ declare namespace LocalJSX { * Custom CSS class to apply to the host element. */ "customClass"?: string; + /** + * The name of the logo to display. Supports any valid 'logo-name' from the 'modus-wc-logo' component. Defaults to 'trimble'. + */ + "logoName"?: LogoName; /** * The open state of the main menu. */ @@ -4468,6 +4489,10 @@ declare namespace LocalJSX { * Event emitted when the help button is clicked or activated via keyboard. */ "onHelpClick"?: (event: ModusWcNavbarCustomEvent) => void; + /** + * Event emitted when the logo is clicked or activated via keyboard. + */ + "onLogoClick"?: (event: ModusWcNavbarCustomEvent) => void; /** * Event emitted when the main menu open state changes. */ @@ -4501,7 +4526,7 @@ declare namespace LocalJSX { */ "onSignOutClick"?: (event: ModusWcNavbarCustomEvent) => void; /** - * Event emitted when the Trimble logo is clicked or activated via keyboard. + * Deprecated: Use logoClick instead. This event will be removed in a future release. */ "onTrimbleLogoClick"?: (event: ModusWcNavbarCustomEvent) => void; /** @@ -5936,6 +5961,10 @@ declare module "@stencil/core" { /** * A customizable navbar component used for top level navigation of all Trimble applications. * The component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML. + * ⚠️ Deprecation Alert + * The `trimbleLogoClick` event is deprecated and will be removed in a future major version. + * Please use the `logoClick` event instead, which serves the same purpose and is not tied to a specific logo name. + * The `logoClick` event will be emitted whenever the logo is clicked, regardless of the `logoName` prop value. */ "modus-wc-navbar": LocalJSX.ModusWcNavbar & JSXBase.HTMLAttributes; /** diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index ae7813bbe0..d57376b3eb 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -56,3 +56,15 @@ modus-wc-content-tree { } } } + +[data-theme='modus-classic-dark'], +[data-theme='modus-modern-dark'], +[data-theme='connect-dark'] { + modus-wc-content-tree { + .modus-wc-content-tree-actions { + .modus-wc-content-tree-action-icon { + color: var(--modus-wc-color-gray-light); + } + } + } +} diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss index 9e498a1172..a9ffccfb8f 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss @@ -129,14 +129,12 @@ modus-wc-tree-item { [data-theme='modus-modern-dark'], [data-theme='connect-dark'] { modus-wc-tree-item { - .modus-wc-tree-content { - &.modus-wc-tree-item-active { - background-color: color-mix( - in sRGB, - var(--modus-wc-color-primary) 30%, - transparent - ); - } + .modus-wc-tree-item-selected > .modus-wc-tree-content { + background-color: color-mix( + in sRGB, + var(--modus-wc-color-primary) 30%, + transparent + ); } } } diff --git a/src/components/modus-wc-logo/README.md b/src/components/modus-wc-logo/README.md index 2c1094d4ac..43b907a01d 100644 --- a/src/components/modus-wc-logo/README.md +++ b/src/components/modus-wc-logo/README.md @@ -20,6 +20,19 @@ Provides consistent branding across applications with various product logo optio | `name` _(required)_ | `name` | The name of the logo to display. Accepts values like 'trimble', 'viewpoint_field_view', etc. | `"trimble" \| "siteworks" \| "earthworks" \| "financials" \| "worksmanager" \| "connect" \| "unity_construct" \| "trade_servicelive" \| "buildable" \| "livecount" \| "supplier_xchange" \| "app_xchange" \| "trimble_unity" \| "sketchup" \| "pc_miler" \| "copilot" \| "trimble_pay" \| "projectsight" \| "demand_planning" \| "viewpoint" \| "viewpoint_analytics" \| "viewpoint_epayments" \| "viewpoint_estimating" \| "viewpoint_field_management" \| "viewpoint_field_time" \| "viewpoint_financial_controls" \| "viewpoint_hr_management" \| "viewpoint_jobpac_connect" \| "viewpoint_procontractor" \| "viewpoint_spectrum" \| "viewpoint_team" \| "viewpoint_vista" \| "viewpoint_spectrum_service_tech" \| "viewpoint_for_projects" \| "viewpoint_vista_field_service" \| "viewpoint_field_view"` | `undefined` | +## Dependencies + +### Used by + + - [modus-wc-navbar](../modus-wc-navbar) + +### Graph +```mermaid +graph TD; + modus-wc-navbar --> modus-wc-logo + style modus-wc-logo fill:#f9f,stroke:#333,stroke-width:4px +``` + ---------------------------------------------- *Built with [StencilJS](https://stenciljs.com/)* diff --git a/src/components/modus-wc-navbar/readme.md b/src/components/modus-wc-navbar/readme.md index b2c95f41a7..83edebc6c1 100644 --- a/src/components/modus-wc-navbar/readme.md +++ b/src/components/modus-wc-navbar/readme.md @@ -11,22 +11,30 @@ A customizable navbar component used for top level navigation of all Trimble app The component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML. +⚠️ Deprecation Alert + + +The `trimbleLogoClick` event is deprecated and will be removed in a future major version. +Please use the `logoClick` event instead, which serves the same purpose and is not tied to a specific logo name. +The `logoClick` event will be emitted whenever the logo is clicked, regardless of the `logoName` prop value. + ## Properties -| Property | Attribute | Description | Type | Default | -| ----------------------- | ------------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `appsMenuOpen` | `apps-menu-open` | The open state of the apps menu. | `boolean \| undefined` | `false` | -| `condensed` | `condensed` | Applies condensed layout and styling. | `boolean \| undefined` | `false` | -| `condensedMenuOpen` | `condensed-menu-open` | The open state of the condensed menu. | `boolean \| undefined` | `false` | -| `customClass` | `custom-class` | Custom CSS class to apply to the host element. | `string \| undefined` | `''` | -| `mainMenuOpen` | `main-menu-open` | The open state of the main menu. | `boolean \| undefined` | `false` | -| `notificationsMenuOpen` | `notifications-menu-open` | The open state of the notifications menu. | `boolean \| undefined` | `false` | -| `searchDebounceMs` | `search-debounce-ms` | Debounce time in milliseconds for search input changes. Default is 300ms. | `number \| undefined` | `300` | -| `searchInputOpen` | `search-input-open` | The open state of the search input. | `boolean \| undefined` | `false` | -| `textOverrides` | `text-overrides` | Text replacements for the navbar. | `INavbarTextOverrides \| undefined` | `undefined` | -| `userCard` _(required)_ | `user-card` | User information used to render the user card. | `INavbarUserCard` | `undefined` | -| `userMenuOpen` | `user-menu-open` | The open state of the user menu. | `boolean \| undefined` | `false` | -| `visibility` | `visibility` | The visibility of individual navbar buttons. Default is user profile visible, others hidden. | `INavbarVisibility \| undefined` | `{ ai: false, apps: false, help: false, mainMenu: false, notifications: false, search: false, searchInput: false, user: true, }` | +| Property | Attribute | Description | Type | Default | +| ----------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `appsMenuOpen` | `apps-menu-open` | The open state of the apps menu. | `boolean \| undefined` | `false` | +| `condensed` | `condensed` | Applies condensed layout and styling. | `boolean \| undefined` | `false` | +| `condensedMenuOpen` | `condensed-menu-open` | The open state of the condensed menu. | `boolean \| undefined` | `false` | +| `customClass` | `custom-class` | Custom CSS class to apply to the host element. | `string \| undefined` | `''` | +| `logoName` | `logo-name` | The name of the logo to display. Supports any valid 'logo-name' from the 'modus-wc-logo' component. Defaults to 'trimble'. | `LogoName \| undefined` | `'trimble'` | +| `mainMenuOpen` | `main-menu-open` | The open state of the main menu. | `boolean \| undefined` | `false` | +| `notificationsMenuOpen` | `notifications-menu-open` | The open state of the notifications menu. | `boolean \| undefined` | `false` | +| `searchDebounceMs` | `search-debounce-ms` | Debounce time in milliseconds for search input changes. Default is 300ms. | `number \| undefined` | `300` | +| `searchInputOpen` | `search-input-open` | The open state of the search input. | `boolean \| undefined` | `false` | +| `textOverrides` | `text-overrides` | Text replacements for the navbar. | `INavbarTextOverrides \| undefined` | `undefined` | +| `userCard` _(required)_ | `user-card` | User information used to render the user card. | `INavbarUserCard` | `undefined` | +| `userMenuOpen` | `user-menu-open` | The open state of the user menu. | `boolean \| undefined` | `false` | +| `visibility` | `visibility` | The visibility of individual navbar buttons. Default is user profile visible, others hidden. | `INavbarVisibility \| undefined` | `{ ai: false, apps: false, help: false, mainMenu: false, notifications: false, search: false, searchInput: false, user: true, }` | ## Events @@ -38,6 +46,7 @@ The component supports a 'main-menu', 'notifications', and 'apps' for inj | `appsMenuOpenChange` | Event emitted when the apps menu open state changes. | `CustomEvent` | | `condensedMenuOpenChange` | Event emitted when the condensed menu open state changes. | `CustomEvent` | | `helpClick` | Event emitted when the help button is clicked or activated via keyboard. | `CustomEvent` | +| `logoClick` | Event emitted when the logo is clicked or activated via keyboard. | `CustomEvent` | | `mainMenuOpenChange` | Event emitted when the main menu open state changes. | `CustomEvent` | | `myTrimbleClick` | Event emitted when the user profile Access MyTrimble button is clicked or activated via keyboard. | `CustomEvent` | | `notificationsClick` | Event emitted when the notifications button is clicked or activated via keyboard. | `CustomEvent` | @@ -46,7 +55,7 @@ The component supports a 'main-menu', 'notifications', and 'apps' for inj | `searchClick` | Event emitted when the search button is clicked or activated via keyboard. | `CustomEvent` | | `searchInputOpenChange` | Event emitted when the search input open state changes. | `CustomEvent` | | `signOutClick` | Event emitted when the user profile sign out button is clicked or activated via keyboard. | `CustomEvent` | -| `trimbleLogoClick` | Event emitted when the Trimble logo is clicked or activated via keyboard. | `CustomEvent` | +| `trimbleLogoClick` | Deprecated: Use logoClick instead. This event will be removed in a future release. | `CustomEvent` | | `userMenuOpenChange` | Event emitted when the user menu open state changes. | `CustomEvent` | @@ -56,6 +65,7 @@ The component supports a 'main-menu', 'notifications', and 'apps' for inj - [modus-wc-toolbar](../modus-wc-toolbar) - [modus-wc-button](../modus-wc-button) +- [modus-wc-logo](../modus-wc-logo) - [modus-wc-menu](../modus-wc-menu) - [modus-wc-menu-item](../modus-wc-menu-item) - [modus-wc-text-input](../modus-wc-text-input) @@ -67,6 +77,7 @@ The component supports a 'main-menu', 'notifications', and 'apps' for inj graph TD; modus-wc-navbar --> modus-wc-toolbar modus-wc-navbar --> modus-wc-button + modus-wc-navbar --> modus-wc-logo modus-wc-navbar --> modus-wc-menu modus-wc-navbar --> modus-wc-menu-item modus-wc-navbar --> modus-wc-text-input diff --git a/src/custom-elements.json b/src/custom-elements.json index 12a6634117..62fe85ea0b 100644 --- a/src/custom-elements.json +++ b/src/custom-elements.json @@ -4557,7 +4557,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable navbar component used for top level navigation of all Trimble applications.\r\n\r\nThe component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML.", + "description": "A customizable navbar component used for top level navigation of all Trimble applications.\r\n\r\nThe component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML.\r\n\r\n⚠️ Deprecation Alert\r\n\r\n\r\nThe `trimbleLogoClick` event is deprecated and will be removed in a future major version.\r\nPlease use the `logoClick` event instead, which serves the same purpose and is not tied to a specific logo name.\r\nThe `logoClick` event will be emitted whenever the logo is clicked, regardless of the `logoName` prop value.", "name": "ModusWcNavbar", "members": [ { @@ -4622,6 +4622,15 @@ "text": "string" } }, + { + "name": "logo-name", + "fieldName": "logoName", + "default": "'trimble'", + "description": "The name of the logo to display. Supports any valid 'logo-name' from the 'modus-wc-logo' component. Defaults to 'trimble'.", + "type": { + "text": "LogoName" + } + }, { "name": "main-menu-open", "fieldName": "mainMenuOpen", @@ -4735,6 +4744,14 @@ }, "description": "Event emitted when the help button is clicked or activated via keyboard." }, + { + "kind": "field", + "name": "logoClick", + "type": { + "text": "EventEmitter" + }, + "description": "Event emitted when the logo is clicked or activated via keyboard." + }, { "kind": "field", "name": "mainMenuOpenChange", @@ -4805,7 +4822,8 @@ "type": { "text": "EventEmitter" }, - "description": "Event emitted when the Trimble logo is clicked or activated via keyboard." + "deprecated": "true", + "description": "Deprecated: Use logoClick instead. This event will be removed in a future release." }, { "kind": "field", From f5290ee07cb6441dd3698f582baa3b3cc06ddcc5 Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Wed, 11 Mar 2026 13:03:49 +0530 Subject: [PATCH 33/39] 662 - css fixes --- .../modus-wc-content-tree.scss | 17 +++++++-- .../modus-wc-content-tree.tsx | 1 + .../modus-wc-tree-actions.scss | 13 +++++-- .../modus-wc-tree-actions.tsx | 2 + .../modus-wc-tree-item.scss | 38 +++++++++++++++++-- 5 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index d57376b3eb..e687b12fc6 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -7,7 +7,7 @@ modus-wc-content-tree { background-color: var(--modus-wc-color-base-page); border: 1px solid var(--modus-wc-color-base-100); display: block; - min-width: 320px; + min-width: 290px; width: 100%; .modus-wc-content-tree-actions { @@ -15,8 +15,17 @@ modus-wc-content-tree { display: flex; gap: var(--modus-wc-spacing-xs, 0.5rem); justify-content: flex-end; - margin-top: var(--modus-wc-spacing-md, 1rem); - padding-bottom: var(--modus-wc-spacing-sm, 0.75rem); + padding: var(--modus-wc-spacing-sm, 0.75rem); + + .modus-wc-content-tree-action-button { + background-color: transparent; + + &:hover, + &:active, + &[aria-pressed='true'] { + background-color: transparent; + } + } .modus-wc-content-tree-action-icon { color: var(--modus-wc-color-black); @@ -33,7 +42,7 @@ modus-wc-content-tree { } .modus-wc-content-tree-header { - padding: var(--modus-wc-spacing-md, 1rem); + padding: var(--modus-wc-spacing-sm, 1rem); } .modus-wc-content-tree-empty { diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx index 0964996949..996153f08d 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx @@ -228,6 +228,7 @@ export class ModusWcContentTree { {this.includeActions && (
            this.handleActionClick(action, e)} > (this.moreActionsButton = el as HTMLElement)} onClick={this.handleMoreActionsClick} aria-expanded={this.isDropdownOpen ? 'true' : 'false'} diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss index a9ffccfb8f..a0036d9327 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss @@ -11,13 +11,33 @@ modus-wc-tree-item { .modus-wc-tree-item-selected > .modus-wc-tree-content { background-color: var(--modus-wc-color-blue-pale); border-radius: unset; - color: var(--modus-wc-color-primary); + color: var(--modus-wc-color-base-content); + + .modus-wc-tree-item-label, + modus-wc-icon { + color: var(--modus-wc-color-base-content); + } + + modus-wc-tree-actions .modus-wc-tree-actions-container .modus-wc-tree-action-button { + visibility: visible; + } + } + + .modus-wc-tree-item:focus-visible { + outline: none; + } + + .modus-wc-tree-item:focus-visible > .modus-wc-tree-content { + border-radius: 0; + outline: 2px solid #0063a3; + outline-offset: 0; } .modus-wc-tree-content { align-items: center; display: flex; - gap: var(--modus-wc-spacing-sm, 0.5rem); + gap: var(--modus-wc-font-size-xs); + padding: var(--modus-wc-spacing-xs) var(--modus-wc-font-size-xs); .modus-wc-tree-drag-handle { left: 0; @@ -26,6 +46,16 @@ modus-wc-tree-item { .modus-wc-tree-toggle-btn { background-color: transparent; + + &:hover, + &:active, + &[aria-pressed='true'] { + background-color: transparent; + } + } + + .modus-wc-tree-toggle-icon { + color: var(--modus-wc-color-base-content); } .modus-wc-tree-toggle-button-hidden { @@ -39,7 +69,7 @@ modus-wc-tree-item { &:hover { border-radius: unset; - modus-wc-tree-actions .modus-wc-tree-action-button { + modus-wc-tree-actions .modus-wc-tree-actions-container .modus-wc-tree-action-button { visibility: visible; } } @@ -105,7 +135,7 @@ modus-wc-tree-item { &.modus-wc-tree-dropdown-show { display: block; - margin-inline-start: 1.6rem; + margin-inline-start: 1.3rem; } } From 70deeb86dc42ba40b1a46135a801d1ba4f79cf94 Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:11:07 +0530 Subject: [PATCH 34/39] 662 - fix build issues --- .../__snapshots__/modus-wc-content-tree.spec.ts.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap b/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap index f82da8968f..832d9609a0 100644 --- a/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap +++ b/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap @@ -9,7 +9,7 @@ exports[`modus-wc-content-tree should render with default props 1`] = `
            - +
            From 5250a414e8217025640d52efc035f8f2d90624a2 Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:16:31 +0530 Subject: [PATCH 35/39] 662- build fixes --- .../modus-wc-tree-item/modus-wc-tree-item.scss | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss index a0036d9327..f551a12555 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss @@ -18,7 +18,9 @@ modus-wc-tree-item { color: var(--modus-wc-color-base-content); } - modus-wc-tree-actions .modus-wc-tree-actions-container .modus-wc-tree-action-button { + modus-wc-tree-actions + .modus-wc-tree-actions-container + .modus-wc-tree-action-button { visibility: visible; } } @@ -69,7 +71,9 @@ modus-wc-tree-item { &:hover { border-radius: unset; - modus-wc-tree-actions .modus-wc-tree-actions-container .modus-wc-tree-action-button { + modus-wc-tree-actions + .modus-wc-tree-actions-container + .modus-wc-tree-action-button { visibility: visible; } } From 84f6cdda64a2edb36ce3925230d760b58451602b Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:12:11 +0530 Subject: [PATCH 36/39] 662 - css fixes --- .../modus-wc-content-tree.scss | 14 +++++++------- .../modus-wc-tree-actions.scss | 8 ++++---- .../modus-wc-tree-item/modus-wc-tree-item.scss | 7 +++---- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index e687b12fc6..26624f5f1d 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -13,9 +13,9 @@ modus-wc-content-tree { .modus-wc-content-tree-actions { align-items: center; display: flex; - gap: var(--modus-wc-spacing-xs, 0.5rem); + gap: var(--modus-wc-spacing-xs); justify-content: flex-end; - padding: var(--modus-wc-spacing-sm, 0.75rem); + padding: var(--modus-wc-spacing-sm); .modus-wc-content-tree-action-button { background-color: transparent; @@ -42,25 +42,25 @@ modus-wc-content-tree { } .modus-wc-content-tree-header { - padding: var(--modus-wc-spacing-sm, 1rem); + padding: var(--modus-wc-spacing-sm); } .modus-wc-content-tree-empty { align-items: center; display: flex; flex-direction: column; - gap: var(--modus-wc-spacing-md, 1rem); + gap: var(--modus-wc-spacing-md); justify-content: center; min-height: 500px; padding: 1rem; .modus-wc-content-tree-empty-icon { - color: var(--modus-wc-color-text-secondary, #6b7280); + color: var(--modus-wc-color-gray-6); } .modus-wc-content-tree-empty-text { - color: var(--modus-wc-color-text-secondary, #6b7280); - font-size: var(--modus-wc-font-size-md, 1rem); + color: var(--modus-wc-color-gray-6); + font-size: var(--modus-wc-font-size-md); text-align: center; } } diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss index 51b8a885b3..fc78f30d7d 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss @@ -46,11 +46,11 @@ modus-wc-tree-actions { align-items: center; background: transparent; border: none; - color: var(--modus-wc-color-base-content-hight-contrast); + color: var(--modus-wc-color-base-content-high-contrast); cursor: pointer; display: flex; - gap: var(--modus-wc-spacing-sm, 0.5rem); - padding: var(--modus-wc-spacing-sm, 0.5rem) var(--modus-wc-spacing-md, 1rem); + gap: var(--modus-wc-spacing-sm); + padding: var(--modus-wc-spacing-sm) var(--modus-wc-spacing-md); text-align: start; width: 100%; @@ -65,7 +65,7 @@ modus-wc-tree-actions { } span { - font-size: var(--modus-wc-font-size-sm, 0.875rem); + font-size: var(--modus-wc-font-size-sm); } } } diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss index f551a12555..1a8493b7f8 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss @@ -94,7 +94,7 @@ modus-wc-tree-item { .modus-wc-tree-item-actions { align-items: center; display: flex; - gap: var(--modus-wc-spacing-xs, 0.25rem); + gap: var(--modus-wc-spacing-xs); margin-inline-start: auto; } @@ -126,9 +126,8 @@ modus-wc-tree-item { border: none; cursor: pointer; display: flex; - gap: var(--modus-wc-spacing-sm, 0.5rem); - padding: var(--modus-wc-spacing-xs, 0.25rem) - var(--modus-wc-spacing-sm, 0.5rem); + gap: var(--modus-wc-spacing-sm); + padding: var(--modus-wc-spacing-xs) var(--modus-wc-spacing-sm); text-align: start; width: 100%; } From cf23b19ef9d57c86c88cbc8bda30a5c1a0a1c098 Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:12:23 +0530 Subject: [PATCH 37/39] css fixes --- src/components.d.ts | 100 ++++++++--- .../modus-wc-content-tree.scss | 12 ++ .../modus-wc-content-tree.tsx | 3 +- .../modus-wc-tree-actions.scss | 36 +++- .../modus-wc-tree-actions.tsx | 1 + .../modus-wc-tree-item.scss | 13 +- src/custom-elements.json | 164 ++++++++++++++++-- 7 files changed, 288 insertions(+), 41 deletions(-) diff --git a/src/components.d.ts b/src/components.d.ts index dc7a360e34..0d81d19c49 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -13,6 +13,7 @@ import { LoaderColor, LoaderVariant } from "./components/modus-wc-loader/modus-w import { LogoName } from "./components/modus-wc-logo/logo-constants"; import { INavbarTextOverrides, INavbarUserCard, INavbarVisibility } from "./components/modus-wc-navbar/modus-wc-navbar"; import { IAriaLabelValues, IPageChange } from "./components/modus-wc-pagination/modus-wc-pagination"; +import { IProfileMenuProps, ISubMenu } from "./components/modus-wc-profile-menu/modus-wc-profile-menu"; import { IRatingChange, ModusWcRatingVariant } from "./components/modus-wc-rating/modus-wc-rating"; import { ISelectOption } from "./components/modus-wc-select/modus-wc-select"; import { IStepperItem } from "./components/modus-wc-stepper/modus-wc-stepper"; @@ -32,6 +33,7 @@ export { LoaderColor, LoaderVariant } from "./components/modus-wc-loader/modus-w export { LogoName } from "./components/modus-wc-logo/logo-constants"; export { INavbarTextOverrides, INavbarUserCard, INavbarVisibility } from "./components/modus-wc-navbar/modus-wc-navbar"; export { IAriaLabelValues, IPageChange } from "./components/modus-wc-pagination/modus-wc-pagination"; +export { IProfileMenuProps, ISubMenu } from "./components/modus-wc-profile-menu/modus-wc-profile-menu"; export { IRatingChange, ModusWcRatingVariant } from "./components/modus-wc-rating/modus-wc-rating"; export { ISelectOption } from "./components/modus-wc-select/modus-wc-select"; export { IStepperItem } from "./components/modus-wc-stepper/modus-wc-stepper"; @@ -1068,11 +1070,8 @@ export namespace Components { } /** * A customizable navbar component used for top level navigation of all Trimble applications. - * The component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML. - * ⚠️ Deprecation Alert - * The `trimbleLogoClick` event is deprecated and will be removed in a future major version. - * Please use the `logoClick` event instead, which serves the same purpose and is not tied to a specific logo name. - * The `logoClick` event will be emitted whenever the logo is clicked, regardless of the `logoName` prop value. + * ⚠️ **Deprecated**: The `user-card` prop will be replaced by `profile-props` prop of the `modus-wc-profile-menu` component in an upcoming release. + * The component requires a profileProps object with user information and optionally accepts menuOne and menuTwo for custom menus. */ interface ModusWcNavbar { /** @@ -1117,6 +1116,7 @@ export namespace Components { "textOverrides"?: INavbarTextOverrides; /** * User information used to render the user card. + * @deprecated The `user-card` prop will be replaced by `profile-props` prop of the `modus-wc-profile-menu` component in an upcoming release. */ "userCard": INavbarUserCard; /** @@ -1264,6 +1264,20 @@ export namespace Components { */ "width"?: string; } + interface ModusWcProfileMenu { + /** + * Configuration for the first menu including title and items + */ + "menuOne"?: ISubMenu; + /** + * Configuration for the second menu including title and items + */ + "menuTwo"?: ISubMenu; + /** + * Profile menu properties containing user information + */ + "profileProps": IProfileMenuProps; + } /** * A customizable progress component used to show the progress of a task or show the passing of time. * The radial variant supports slotting in custom HTML to be displayed within the progress circle. @@ -2223,6 +2237,10 @@ export interface ModusWcPaginationCustomEvent extends CustomEvent { detail: T; target: HTMLModusWcPaginationElement; } +export interface ModusWcProfileMenuCustomEvent extends CustomEvent { + detail: T; + target: HTMLModusWcProfileMenuElement; +} export interface ModusWcRadioCustomEvent extends CustomEvent { detail: T; target: HTMLModusWcRadioElement; @@ -2736,17 +2754,13 @@ declare global { "searchClick": MouseEvent | KeyboardEvent; "searchInputOpenChange": boolean; "signOutClick": MouseEvent | KeyboardEvent; - "logoClick": MouseEvent | KeyboardEvent; "trimbleLogoClick": MouseEvent | KeyboardEvent; "userMenuOpenChange": boolean; } /** * A customizable navbar component used for top level navigation of all Trimble applications. - * The component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML. - * ⚠️ Deprecation Alert - * The `trimbleLogoClick` event is deprecated and will be removed in a future major version. - * Please use the `logoClick` event instead, which serves the same purpose and is not tied to a specific logo name. - * The `logoClick` event will be emitted whenever the logo is clicked, regardless of the `logoName` prop value. + * ⚠️ **Deprecated**: The `user-card` prop will be replaced by `profile-props` prop of the `modus-wc-profile-menu` component in an upcoming release. + * The component requires a profileProps object with user information and optionally accepts menuOne and menuTwo for custom menus. */ interface HTMLModusWcNavbarElement extends Components.ModusWcNavbar, HTMLStencilElement { addEventListener(type: K, listener: (this: HTMLModusWcNavbarElement, ev: ModusWcNavbarCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; @@ -2814,6 +2828,24 @@ declare global { prototype: HTMLModusWcPanelElement; new (): HTMLModusWcPanelElement; }; + interface HTMLModusWcProfileMenuElementEventMap { + "signOutClick": void; + "menuItemClick": string; + } + interface HTMLModusWcProfileMenuElement extends Components.ModusWcProfileMenu, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLModusWcProfileMenuElement, ev: ModusWcProfileMenuCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLModusWcProfileMenuElement, ev: ModusWcProfileMenuCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; + } + var HTMLModusWcProfileMenuElement: { + prototype: HTMLModusWcProfileMenuElement; + new (): HTMLModusWcProfileMenuElement; + }; /** * A customizable progress component used to show the progress of a task or show the passing of time. * The radial variant supports slotting in custom HTML to be displayed within the progress circle. @@ -3292,6 +3324,7 @@ declare global { "modus-wc-number-input": HTMLModusWcNumberInputElement; "modus-wc-pagination": HTMLModusWcPaginationElement; "modus-wc-panel": HTMLModusWcPanelElement; + "modus-wc-profile-menu": HTMLModusWcProfileMenuElement; "modus-wc-progress": HTMLModusWcProgressElement; "modus-wc-radio": HTMLModusWcRadioElement; "modus-wc-rating": HTMLModusWcRatingElement; @@ -4434,11 +4467,8 @@ declare namespace LocalJSX { } /** * A customizable navbar component used for top level navigation of all Trimble applications. - * The component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML. - * ⚠️ Deprecation Alert - * The `trimbleLogoClick` event is deprecated and will be removed in a future major version. - * Please use the `logoClick` event instead, which serves the same purpose and is not tied to a specific logo name. - * The `logoClick` event will be emitted whenever the logo is clicked, regardless of the `logoName` prop value. + * ⚠️ **Deprecated**: The `user-card` prop will be replaced by `profile-props` prop of the `modus-wc-profile-menu` component in an upcoming release. + * The component requires a profileProps object with user information and optionally accepts menuOne and menuTwo for custom menus. */ interface ModusWcNavbar { /** @@ -4489,10 +4519,6 @@ declare namespace LocalJSX { * Event emitted when the help button is clicked or activated via keyboard. */ "onHelpClick"?: (event: ModusWcNavbarCustomEvent) => void; - /** - * Event emitted when the logo is clicked or activated via keyboard. - */ - "onLogoClick"?: (event: ModusWcNavbarCustomEvent) => void; /** * Event emitted when the main menu open state changes. */ @@ -4526,7 +4552,7 @@ declare namespace LocalJSX { */ "onSignOutClick"?: (event: ModusWcNavbarCustomEvent) => void; /** - * Deprecated: Use logoClick instead. This event will be removed in a future release. + * Event emitted when the logo button is clicked or activated via keyboard,regardless of the `logoName` prop value. */ "onTrimbleLogoClick"?: (event: ModusWcNavbarCustomEvent) => void; /** @@ -4547,6 +4573,7 @@ declare namespace LocalJSX { "textOverrides"?: INavbarTextOverrides; /** * User information used to render the user card. + * @deprecated The `user-card` prop will be replaced by `profile-props` prop of the `modus-wc-profile-menu` component in an upcoming release. */ "userCard": INavbarUserCard; /** @@ -4710,6 +4737,28 @@ declare namespace LocalJSX { */ "width"?: string; } + interface ModusWcProfileMenu { + /** + * Configuration for the first menu including title and items + */ + "menuOne"?: ISubMenu; + /** + * Configuration for the second menu including title and items + */ + "menuTwo"?: ISubMenu; + /** + * Emitted when any menu item is clicked, passing back the item value or label + */ + "onMenuItemClick"?: (event: ModusWcProfileMenuCustomEvent) => void; + /** + * Emitted when the Sign Out menu item is clicked + */ + "onSignOutClick"?: (event: ModusWcProfileMenuCustomEvent) => void; + /** + * Profile menu properties containing user information + */ + "profileProps": IProfileMenuProps; + } /** * A customizable progress component used to show the progress of a task or show the passing of time. * The radial variant supports slotting in custom HTML to be displayed within the progress circle. @@ -5803,6 +5852,7 @@ declare namespace LocalJSX { "modus-wc-number-input": ModusWcNumberInput; "modus-wc-pagination": ModusWcPagination; "modus-wc-panel": ModusWcPanel; + "modus-wc-profile-menu": ModusWcProfileMenu; "modus-wc-progress": ModusWcProgress; "modus-wc-radio": ModusWcRadio; "modus-wc-rating": ModusWcRating; @@ -5960,11 +6010,8 @@ declare module "@stencil/core" { "modus-wc-modal": LocalJSX.ModusWcModal & JSXBase.HTMLAttributes; /** * A customizable navbar component used for top level navigation of all Trimble applications. - * The component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML. - * ⚠️ Deprecation Alert - * The `trimbleLogoClick` event is deprecated and will be removed in a future major version. - * Please use the `logoClick` event instead, which serves the same purpose and is not tied to a specific logo name. - * The `logoClick` event will be emitted whenever the logo is clicked, regardless of the `logoName` prop value. + * ⚠️ **Deprecated**: The `user-card` prop will be replaced by `profile-props` prop of the `modus-wc-profile-menu` component in an upcoming release. + * The component requires a profileProps object with user information and optionally accepts menuOne and menuTwo for custom menus. */ "modus-wc-navbar": LocalJSX.ModusWcNavbar & JSXBase.HTMLAttributes; /** @@ -5980,6 +6027,7 @@ declare module "@stencil/core" { * This component provides 'header', 'body', and 'footer' `` elements for inserting custom HTML. */ "modus-wc-panel": LocalJSX.ModusWcPanel & JSXBase.HTMLAttributes; + "modus-wc-profile-menu": LocalJSX.ModusWcProfileMenu & JSXBase.HTMLAttributes; /** * A customizable progress component used to show the progress of a task or show the passing of time. * The radial variant supports slotting in custom HTML to be displayed within the progress circle. diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index 26624f5f1d..9326c41551 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -16,6 +16,7 @@ modus-wc-content-tree { gap: var(--modus-wc-spacing-xs); justify-content: flex-end; padding: var(--modus-wc-spacing-sm); + padding-top: var(--modus-wc-spacing-xs); .modus-wc-content-tree-action-button { background-color: transparent; @@ -70,6 +71,17 @@ modus-wc-content-tree { [data-theme='modus-modern-dark'], [data-theme='connect-dark'] { modus-wc-content-tree { + modus-wc-button + .modus-wc-btn.modus-wc-btn-borderless.modus-wc-btn-primary.modus-wc-content-tree-action-button { + color: var(--modus-wc-color-gray-light); + + &:hover, + &:active, + &[aria-pressed='true'] { + color: var(--modus-wc-color-gray-light); + } + } + .modus-wc-content-tree-actions { .modus-wc-content-tree-action-icon { color: var(--modus-wc-color-gray-light); diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx index 996153f08d..3cf25e7a54 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.tsx @@ -218,6 +218,7 @@ export class ModusWcContentTree { value={this.searchValue} include-clear include-search + size="sm" customClass="modus-wc-content-tree-search-input" onKeyDown={this.handleInputKeyDown} onInputChange={this.handleInputChange} @@ -225,7 +226,7 @@ export class ModusWcContentTree {
            )} - {this.includeActions && ( + {this.includeActions && this.hasSlotContent && (
            .modus-wc-tree-content { background-color: var(--modus-wc-color-blue-pale); border-radius: unset; - color: var(--modus-wc-color-base-content); + color: var(--modus-wc-color-trimble-blue); .modus-wc-tree-item-label, modus-wc-icon { @@ -37,8 +37,9 @@ modus-wc-tree-item { .modus-wc-tree-content { align-items: center; + color: var(--modus-wc-color-base-content); display: flex; - gap: var(--modus-wc-font-size-xs); + gap: var(--modus-wc-font-size-sm); padding: var(--modus-wc-spacing-xs) var(--modus-wc-font-size-xs); .modus-wc-tree-drag-handle { @@ -69,6 +70,7 @@ modus-wc-tree-item { } &:hover { + background-color: var(--modus-wc-color-gray-0); border-radius: unset; modus-wc-tree-actions @@ -162,6 +164,13 @@ modus-wc-tree-item { [data-theme='modus-modern-dark'], [data-theme='connect-dark'] { modus-wc-tree-item { + .modus-wc-tree-content { + &:hover { + background-color: var(--modus-wc-color-gray-9); + color: var(--modus-wc-color-gray-light); + } + } + .modus-wc-tree-item-selected > .modus-wc-tree-content { background-color: color-mix( in sRGB, diff --git a/src/custom-elements.json b/src/custom-elements.json index 62fe85ea0b..273f258029 100644 --- a/src/custom-elements.json +++ b/src/custom-elements.json @@ -4557,7 +4557,7 @@ "declarations": [ { "kind": "class", - "description": "A customizable navbar component used for top level navigation of all Trimble applications.\r\n\r\nThe component supports a 'main-menu', 'notifications', and 'apps' for injecting custom HTML menus. It also supports a 'start', 'center', and 'end' `` for injecting additional custom HTML.\r\n\r\n⚠️ Deprecation Alert\r\n\r\n\r\nThe `trimbleLogoClick` event is deprecated and will be removed in a future major version.\r\nPlease use the `logoClick` event instead, which serves the same purpose and is not tied to a specific logo name.\r\nThe `logoClick` event will be emitted whenever the logo is clicked, regardless of the `logoName` prop value.", + "description": "A customizable navbar component used for top level navigation of all Trimble applications.\r\n\r\n⚠️ **Deprecated**: The `user-card` prop will be replaced by `profile-props` prop of the `modus-wc-profile-menu` component in an upcoming release.\r\nThe component requires a profileProps object with user information and optionally accepts menuOne and menuTwo for custom menus.", "name": "ModusWcNavbar", "members": [ { @@ -4744,14 +4744,6 @@ }, "description": "Event emitted when the help button is clicked or activated via keyboard." }, - { - "kind": "field", - "name": "logoClick", - "type": { - "text": "EventEmitter" - }, - "description": "Event emitted when the logo is clicked or activated via keyboard." - }, { "kind": "field", "name": "mainMenuOpenChange", @@ -4822,8 +4814,7 @@ "type": { "text": "EventEmitter" }, - "deprecated": "true", - "description": "Deprecated: Use logoClick instead. This event will be removed in a future release." + "description": "Event emitted when the logo button is clicked or activated via keyboard,regardless of the `logoName` prop value." }, { "kind": "field", @@ -5295,6 +5286,157 @@ } ] }, + { + "kind": "javascript-module", + "path": "src/components/modus-wc-profile-menu/modus-wc-profile-menu.tsx", + "declarations": [ + { + "kind": "class", + "description": "", + "name": "ModusWcProfileMenu", + "members": [ + { + "kind": "field", + "name": "el", + "type": { + "text": "HTMLElement" + } + }, + { + "kind": "method", + "name": "handleItemSelect", + "parameters": [ + { + "name": "event", + "type": { + "text": "CustomEvent" + } + } + ] + }, + { + "kind": "method", + "name": "render" + } + ], + "attributes": [ + { + "name": "menu-one", + "fieldName": "menuOne", + "description": "Configuration for the first menu including title and items", + "type": { + "text": "ISubMenu" + } + }, + { + "name": "menu-two", + "fieldName": "menuTwo", + "description": "Configuration for the second menu including title and items", + "type": { + "text": "ISubMenu" + } + }, + { + "name": "profile-props", + "fieldName": "profileProps", + "description": "Profile menu properties containing user information", + "type": { + "text": "IProfileMenuProps" + } + } + ], + "tagName": "modus-wc-profile-menu", + "events": [ + { + "kind": "field", + "name": "menuItemClick", + "type": { + "text": "EventEmitter" + }, + "description": "Emitted when any menu item is clicked, passing back the item value or label" + }, + { + "kind": "field", + "name": "signOutClick", + "type": { + "text": "EventEmitter" + }, + "description": "Emitted when the Sign Out menu item is clicked" + } + ], + "customElement": true + } + ], + "exports": [ + { + "kind": "js", + "name": "ModusWcProfileMenu", + "declaration": { + "name": "ModusWcProfileMenu", + "module": "src/components/modus-wc-profile-menu/modus-wc-profile-menu.tsx" + } + }, + { + "kind": "custom-element-definition", + "name": "modus-wc-profile-menu", + "declaration": { + "name": "ModusWcProfileMenu", + "module": "src/components/modus-wc-profile-menu/modus-wc-profile-menu.tsx" + } + } + ] + }, + { + "kind": "javascript-module", + "path": "src/components/modus-wc-profile-menu/utils/menu_template.tsx", + "declarations": [ + { + "kind": "function", + "name": "renderSubMenu", + "parameters": [ + { + "name": "subMenu", + "optional": true, + "type": { + "text": "ISubMenu" + }, + "description": "The submenu configuration containing title and items" + }, + { + "name": "onMenuItemClick", + "optional": true, + "type": { + "text": "(value: string) => void" + }, + "description": "Callback function when a menu item is clicked" + }, + { + "name": "isMainMenu", + "optional": true, + "type": { + "text": "boolean" + } + } + ], + "description": "Renders a submenu section with optional title and menu items", + "return": { + "type": { + "text": "" + } + } + } + ], + "exports": [ + { + "kind": "js", + "name": "renderSubMenu", + "declaration": { + "name": "renderSubMenu", + "module": "src/components/modus-wc-profile-menu/utils/menu_template.tsx" + } + } + ] + }, { "kind": "javascript-module", "path": "src/components/modus-wc-progress/modus-wc-progress.tsx", From 223cc21dc2d7dd71d16b972ce0f3beef5f9a9dbb Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:57:13 +0530 Subject: [PATCH 38/39] 662 - ui fixes --- .../modus-wc-content-tree.scss | 2 +- .../modus-wc-tree-actions.scss | 10 ------- .../modus-wc-tree-item.scss | 29 +++++++------------ 3 files changed, 11 insertions(+), 30 deletions(-) diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss index 9326c41551..a392c9d16d 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.scss +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.scss @@ -18,7 +18,7 @@ modus-wc-content-tree { padding: var(--modus-wc-spacing-sm); padding-top: var(--modus-wc-spacing-xs); - .modus-wc-content-tree-action-button { + button.modus-wc-content-tree-action-button { background-color: transparent; &:hover, diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss index 83f6853bf2..2b4ab1e4aa 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-actions/modus-wc-tree-actions.scss @@ -85,16 +85,6 @@ modus-wc-tree-actions { } } - .modus-wc-tree-actions-container { - .modus-wc-tree-action-button { - color: var(--modus-wc-color-base-content); - - &:hover { - color: var(--modus-wc-color-base-content); - } - } - } - .modus-wc-tree-more-actions-dropdown { background: var(--modus-wc-color-trimble-gray); border-color: var(--modus-wc-color-gray-6); diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss index 1cae09d9a4..be5e5fbfc8 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss @@ -13,11 +13,6 @@ modus-wc-tree-item { border-radius: unset; color: var(--modus-wc-color-trimble-blue); - .modus-wc-tree-item-label, - modus-wc-icon { - color: var(--modus-wc-color-base-content); - } - modus-wc-tree-actions .modus-wc-tree-actions-container .modus-wc-tree-action-button { @@ -39,9 +34,13 @@ modus-wc-tree-item { align-items: center; color: var(--modus-wc-color-base-content); display: flex; - gap: var(--modus-wc-font-size-sm); + gap: 0; padding: var(--modus-wc-spacing-xs) var(--modus-wc-font-size-xs); + .modus-wc-tree-item-actions { + margin-inline-start: 4px; + } + .modus-wc-tree-drag-handle { left: 0; position: absolute; @@ -57,6 +56,10 @@ modus-wc-tree-item { } } + [slot='start-icon'] { + padding-inline: 10px; + } + .modus-wc-tree-toggle-icon { color: var(--modus-wc-color-base-content); } @@ -87,6 +90,7 @@ modus-wc-tree-item { .modus-wc-tree-item-labels { flex: 1; + margin-inline-start: 4px; .modus-wc-tree-item-label { display: block; @@ -97,7 +101,6 @@ modus-wc-tree-item { align-items: center; display: flex; gap: var(--modus-wc-spacing-xs); - margin-inline-start: auto; } .modus-wc-tree-toggle-spacer { @@ -122,18 +125,6 @@ modus-wc-tree-item { width: 2.25rem; } - button { - align-items: center; - background: transparent; - border: none; - cursor: pointer; - display: flex; - gap: var(--modus-wc-spacing-sm); - padding: var(--modus-wc-spacing-xs) var(--modus-wc-spacing-sm); - text-align: start; - width: 100%; - } - .modus-wc-tree-dropdown { display: none; list-style: none; From 6aa9e0f6956d6fe07d020764102a043d9be6c9c7 Mon Sep 17 00:00:00 2001 From: Prashanth R <168108000+prashanthr6383@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:07:23 +0530 Subject: [PATCH 39/39] 662 - code fixes --- .../__snapshots__/modus-wc-content-tree.spec.ts.snap | 7 +------ .../modus-wc-content-tree.spec.ts | 12 ++++++++++-- .../modus-wc-tree-item/modus-wc-tree-item.scss | 4 ++++ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap b/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap index 832d9609a0..aef9416143 100644 --- a/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap +++ b/src/components/modus-wc-content-tree/__snapshots__/modus-wc-content-tree.spec.ts.snap @@ -6,12 +6,7 @@ exports[`modus-wc-content-tree should render with default props 1`] = `
            -
            - - - +
            diff --git a/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts b/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts index cd4d2c3f2a..59b5a277e9 100644 --- a/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts +++ b/src/components/modus-wc-content-tree/modus-wc-content-tree.spec.ts @@ -352,7 +352,11 @@ describe('modus-wc-content-tree', () => { it('renders expand/collapse button with "Expand all" aria-label when collapsed', async () => { const page = await newSpecPage({ components: [ModusWcContentTree], - html: '', + html: ` + + + + `, }); const tree = page.rootInstance; @@ -369,7 +373,11 @@ describe('modus-wc-content-tree', () => { it('renders expand/collapse button with "Collapse all" aria-label when expanded', async () => { const page = await newSpecPage({ components: [ModusWcContentTree], - html: '', + html: ` + + + + `, }); const tree = page.rootInstance; diff --git a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss index be5e5fbfc8..287e3ef980 100644 --- a/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss +++ b/src/components/modus-wc-content-tree/modus-wc-tree-item/modus-wc-tree-item.scss @@ -60,6 +60,10 @@ modus-wc-tree-item { padding-inline: 10px; } + modus-wc-checkbox { + padding-inline: 10px; + } + .modus-wc-tree-toggle-icon { color: var(--modus-wc-color-base-content); }