Skip to main content

devtools-ui-widgets

Guidelines for building UI widgets using the MVP architecture in DevTools. Covers Widget lifecycle, lit-html views, and state management.

ソース情報

リポジトリ
ChromeDevTools/devtools-frontend
ソースの最終更新活動
2026年11月11日 08:57
検出された SKILL.md の言語
英語
スター
4,090
フォーク
736

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
devtools-ui-widgets
description
Guidelines for building UI widgets using the MVP architecture in DevTools. Covers Widget lifecycle, lit-html views, and state management.
## UI Widget Framework Guide (MVP Architecture) Adhere strictly to the Model-View-Presenter (MVP) architecture. * **Models** (`front_end/models/`): Handle business logic and application state. MUST NOT have knowledge of the UI. * **Presenters (`Widget`)**: Orchestrate data flow, manage UI-specific state, and handle interactions. * **Views (`function`)**: Purely presentational. Stateless functions that receive data/callbacks and render using `lit-html`. **Refactoring Note:** When refactoring legacy widgets to this framework, the widget's class name MUST NOT be changed. If you're refactoring a UI piece that uses shadow root (e.g. custom HTMLElements) to a Widget, make sure to update its styles to wrap the rules with a `@scope to (devtools-widget > *)` rule. ### Presenter (`Widget`) Rules * Location: Co-located in the same file as its View. * MUST extend a base `UI.Widget` class (e.g., `UI.Widget.Widget`). Note that `UI.Widget.Widget` is not an `HTMLElement` and must be appended via `.show()` or `UI.Widget.widget` * Constructor MUST assign the injected view function to a private `#view` field. * Constructor MUST call `super()`. If taking `element?: HTMLElement`, pass it to `super(element)`. `super(true)` is forbidden. Do not pass `classes` or `jslog` in `WidgetOptions` (`super(..., {classes: [...], jslog: ...})`); instead, pass container CSS classes and `jslog` via the third parameter of `render(template, target, {container: {classes: [...], attributes: {jslog: ...}}})` in the view function (and if the widget uses shadow DOM, pass `useShadowDom: 'pure'` and extend `Widget<ShadowRoot>` / `VBox<ShadowRoot>` with `target: HTMLElement|DocumentFragment` so `render()` applies `container.classes` and `container.attributes` to the shadow host `this.element`). * Dependencies from `Universe` SHOULD be requested via static `INJECT`: `static override readonly INJECT = [...] as const;` and received as constructor parameter #2 (`[dep1, dep2]: UI.Widget.WidgetDependencies<typeof MyWidget>`). * Styling MUST be handled within the View. `this.registerCSSFiles()` is forbidden. * Initial render MUST be triggered in `override wasShown(): void` by calling `this.requestUpdate()`. External model, workspace, and setting subscriptions SHOULD be attached in `wasShown()` and detached symmetrically in `willHide()`, reconciling incremental state changes that occurred while hidden rather than unconditionally calling `reset()` and wiping populated presenter state (such as open tree folders or selection) on every re-show. * MUST hold all UI state as private class fields (e.g., `#counter`). * To schedule a UI update, MUST call `this.requestUpdate()` asynchronously across all presenter methods, property setters, and event handlers. `performUpdate()` MUST NOT be called directly (except in `UI.Panel`/`UI.Dialog` subclasses where `requestUpdate()` is unavailable or when synchronous layout measurement is documented as strictly required). Presenter and presenter-node classes MUST NOT expose imperative view-refresh methods (e.g., `updateTrailingIcons()`, `updateIcon()`); all visual output should be derived declaratively in the view function. ### View (`function`) Rules * MUST be a pure function, typically named `DEFAULT_VIEW`, in the same file as the Presenter. * MUST use the signature: `(input: ViewInput, output: ViewOutput, target: HTMLElement)`. * `input`: Data and event handlers from the Presenter. * `output`: Callbacks for imperative actions (e.g., focus) provided to the Presenter. If unused, the `output` parameter should be typed as `undefined` in the view's signature, and `undefined` should be passed from the presenter. * `target`: The HTML element to render into. * The `input` parameter MUST NOT be destructured. Always use `input.propertyName`. * Responsibility: Render a `lit-html` template into `target`. * **Template Formatting & Whitespace:** The custom `html` tag from `ui/lit/lit.js` (`front_end/ui/lit/strip-whitespace.ts`) automatically strips `\n\s*` outside of `<...>` tags at runtime, so multi-line formatting between text interpolations and inline elements (`<span>`, etc.) does not introduce whitespace text nodes. * MUST NOT hold state, fetch data, or perform business logic. #### Helper Functions in Views Complex views can be broken down using helper functions defined inside `DEFAULT_VIEW` to access `input` and `output`. ```ts // clang-format off const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLElement): void => { const renderHeader = (): LitTemplate => { return html`<h1>${input.title}</h1>`; }; // ... render(html` <div class="child-widget"> ${renderHeader()} </div> `, target); }; // clang-format on ``` ### Composition * To render a child widget, the parent's View MUST use the lit-html directive `UI.Widget.widget`. * **Prefer `${widget(WidgetClass, {params})}` directly as a child expression** over `<devtools-widget ${widget(WidgetClass, {params})}></devtools-widget>`. Only use an explicit `<devtools-widget>` tag when you need to set other HTML attributes/classes, apply additional directives (such as `widgetRef` or `ref`), or provide child nodes. * **Strongly prefer the two-argument `widget(WidgetClass, {params})` form** over the single-argument factory callback `widget(element => new WidgetClass(element, ...))`. The two-argument form automatically updates properties and calls `requestUpdate()` on subsequent renders, whereas a factory callback only runs once on initial instantiation. * If a child widget currently exposes imperative setter methods (e.g., `setStorageKey(key)` or `setStorageBucket(bucket)`) instead of public properties/setters, convert them into standard TypeScript setters (`set storageKey(key)`, `set storageBucket(bucket)`) on the child widget so it can be instantiated and updated declaratively via the two-argument `widget(WidgetClass, {params})` directive. * Configuration is passed via the parameters. * Properties passed from a parent MUST be declared as public fields or setters on the child presenter class. * The framework automatically updates these properties and calls `requestUpdate()` on the child when the parent re-renders. * **Avoid `UI.Widget.WrapperWidget`:** Do **NOT** pass `UI.Widget` instances (such as `new ChildWidget()`, `UI.TabbedPane.TabbedPane` / `tabbedLocation.tabbedPane()`, `StackLocation.widget()`, `UI.ListWidget.ListWidget`, or `UI.SplitWidget.SplitWidget`) via `ViewInput` to wrap in `UI.Widget.WrapperWidget`. * **Child widgets:** Refactor the child widget to accept `element?: HTMLElement` in its constructor and setters for its state, and instantiate it declaratively in the view with `${widget(ChildWidget, {...})}`. * **`TabbedPane` and `StackLocation`:** Create tabbed panes and stack panes declaratively in the view using `<devtools-tabbed-pane>` (or `${widget(UI.TabbedPane.TabbedPane, ...)}`) and `<devtools-stack-pane>` rather than passing a `TabbedPane` or `StackLocation.widget()` instance through `ViewInput` to `WrapperWidget`. * **`UI.ListWidget.ListWidget` and `UI.ListWidget.Delegate`:** Do NOT keep `this.list` and `UI.ListWidget.Delegate` on the presenter (calling `this.list.clear()`, `this.list.appendItem()`, `this.list.addNewItem()`, rendering `renderItem`/`beginEdit` into a `DocumentFragment`, and mounting `this.list` via `WrapperWidget`). Replace `ListWidget` + `ListWidget.Delegate` with `<devtools-list>` (`ui/components/lists/lists.js`) driven by pure `ViewInput` data and callbacks (using `<devtools-prompt>`, an inline editor row, or `UI.Dialog.Dialog` for editing). If legacy `ListWidget.Editor` DOM/screenshot goldens must be preserved temporarily in an incremental CL, move `ListWidget` + `ListWidget.Delegate` ownership into the view layer (e.g. a small view-layer child class extending `UI.ListWidget.ListWidget` directly and instantiated via `${widget(...)}` in `DEFAULT_VIEW`, without `WrapperWidget`) so the presenter only passes pure data and callbacks in `ViewInput`. * `WrapperWidget` is only justified when reparenting a shared stateful singleton across multiple containers (such as `ConsoleView.instance()` or `SourcesView`) or bridging an opaque `UI.Widget` returned by an external factory. Remember that `WrapperWidget` sets `display: contents` on its `<devtools-widget>` host element, so never put sizing, border, or background classes on that `<devtools-widget>` host (wrap it in a `<div>` instead). * **Listening to Child Widget Events (`eventMixin`):** When a child `UI.Widget` uses `Common.ObjectWrapper.eventMixin(UI.Widget.Widget)`, calling `this.dispatchEventToListeners(Events.FOO, payload)` also dispatches a non-bubbling `new CustomEvent(Events.FOO, {detail: payload})` on the widget's host `<devtools-widget>` element. Parent Lit templates can listen declaratively with `<devtools-widget ${widget(ChildWidget, {...})} @Foo=${(e: CustomEvent<Payload>) => input.onFoo(e.detail)}></devtools-widget>` (Lit preserves the case of `@Foo` from the template string). This only works on the `<devtools-widget>` element itself (the `CustomEvent` does not bubble) and only for `UI.Widget` classes using `eventMixin` (plain `ObjectWrapper` subclasses do not have `dispatchDOMEvent`). ## Refactoring Legacy Components When migrating imperative components (extending `UI.VBox`, `UI.Panel`, or `HTMLElement`) to the MVP architecture: 1. **Analyze Rendering Logic:** Identify where DOM is created (constructor, `wasShown`, imperative methods). This logic moves to the `DEFAULT_VIEW`. 2. **Convert Base Class:** * Prefer extending `UI.Widget.Widget`. * **Special Case:** If the component *must* extend `UI.Panel.Panel` or `UI.Dialog.Dialog` (to retain specific functionality), you cannot use `requestUpdate()`. Instead, call `this.performUpdate()` directly on state changes. 3. **State Migration:** Move DOM-stored state to private class fields. 4. **Update Usage:** Replace `new MyComponent()` instantiations with declarative `widget(MyComponent, {params})` in parent templates. 5. **Scoping Styles:** Ensure your CSS file uses an `@scope to (devtools-widget > *)` block to prevent style leaks. Land `@scope` CSS wrapping in its own small, standalone CL separate from widget migration CLs so visual regressions are easy to bisect: ```css @scope to (devtools-widget > *) { ... } ``` ## Key Implementation Details (Gotchas) ### Styling Strategy DevTools widgets typically render into Light DOM (the default for `UI.Widget`). To ensure styles are scoped to the component and do not leak into child widgets, wrap your CSS in an `@scope` block: ```css /* myWidget.css */ @scope to (devtools-widget > *) { :scope { display: flex; } .my-class { ... } } ``` Then, include the styles in your `DEFAULT_VIEW`: ```ts import myWidgetStyles from './myWidget.css.js'; // ... render(html` <style>${myWidgetStyles}</style> <div class="my-widget">...</div> `, target); ``` **Automated Lint Enforcement:** - `@devtools/no-unscoped-light-dom-styles` (ESLint): checks stylesheets injected by Light DOM widgets (`<style>${styles}</style>`, `this.registerRequiredCSS()`, `appendStyle()`) and requires all rules to sit inside a top-level `@scope` block. Never add new files to the self-cleaning `burndown` list in `eslint.config.mjs`; remove migrated files from the list once scoped. - `plugin/check_scope_selectors` (stylelint): unconditionally forbids `:host` inside `@scope` (even with `:scope` or `&`), and flags ancestor selectors that receive an implicit `:scope ` descendant combinator and cannot match without `:scope` or `&` (`:host-context()`, `:root`, `html`, `body`, `.theme-with-dark-background`, `.platform-*`, `.overlay-scrollbar-enabled`). **Key `@scope` Gotchas & Rules:** * **`:scope` instead of `:host`**: `:host` is Shadow DOM-only and never matches inside `@scope`. Style the widget's root container element with `:scope`. * **Theme-aware tokens instead of theme or platform classes**: Because un-anchored selectors inside `@scope` receive an implicit `:scope ` prefix, selectors like `.theme-with-dark-background .foo` or `:host-context(...)` look inside the widget for the ancestor class and never match. Use theme-aware design tokens (`--sys-color-*` / `--app-color-*`) instead. * **Shared stylesheets**: Do not inject `inspectorCommonStyles` or `textButtonStyles` into Light DOM views (`installComponentRootStyles()` already injects them globally into the document and shadow roots). Wrap other shared stylesheets inline as `<style>@scope to (devtools-widget > *) { ${styles} }</style>` so Shadow DOM consumers stay unaffected. * **Lower boundary and imperative child widgets**: The `(devtools-widget > *)` lower boundary stops cascading into declarative `<devtools-widget>` children (`${widget(...)}`), but it does **not** stop at legacy imperatively attached `<div class="widget">` children (`childWidget.show(container)`). * **Small, Standalone CLs**: Always land `@scope` CSS wrapping in a dedicated, standalone CL separate from logic, template, or widget-promotion CLs so visual regressions are easy to bisect. **Note:** The `{host: input}` option in `render()` is generally **not required** unless you are using specific Shadow DOM patterns that necessitate it. The `@scope` strategy is the preferred method for style isolation in DevTools widgets. ### Legacy Interop & Refs * **Raw Elements:** Use `Directives.ref` (from `ui/lit/lit.js`) to obtain a reference to a raw `HTMLElement` if needed for imperative APIs (e.g., `splitWidget.installResizer(element)`). * **Child Widgets:** Use `UI.Widget.widgetRef` to obtain the class instance of a child widget inside the view function if you need to call imperative methods on it. **Do NOT store child widget instances on `ViewOutput` as a final result** (doing so is only acceptable temporarily in intermediate migration CLs). Instead, either supply all needed configuration declaratively through setters/inputs on the child widget, or if imperative calls are strictly necessary, expose narrow action functions on `ViewOutput` (e.g., `output.doLayout = () => splitWidget.doLayout()`, `output.revealPosition = pos => childWidget.revealPosition(pos)`). Do not keep `get*ForTest()` methods that return child widget instances via `ViewOutput`; tests should query rendered `<devtools-widget>` elements via `.getWidget()` or assert on `ViewInput` via `createViewFunctionStub`. ### Visibility (`?hidden` Attribute Only) Toggle element visibility declaratively using the HTML `?hidden` attribute: ```ts html`<div ?hidden=${!input.isVisible}>...</div>` ```
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る