Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Tree View] Add React Compiler linting rules #16357

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const ENABLE_REACT_COMPILER_PLUGIN_DATA_GRID =
const ENABLE_REACT_COMPILER_PLUGIN_DATE_PICKERS =
process.env.ENABLE_REACT_COMPILER_PLUGIN_DATE_PICKERS ?? false;
const ENABLE_REACT_COMPILER_PLUGIN_TREE_VIEW =
process.env.ENABLE_REACT_COMPILER_PLUGIN_TREE_VIEW ?? false;
process.env.ENABLE_REACT_COMPILER_PLUGIN_TREE_VIEW ?? true;

const isAnyReactCompilerPluginEnabled =
ENABLE_REACT_COMPILER_PLUGIN ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {
RichTreeViewItems,
} from '@mui/x-tree-view/internals';

const useTreeViewLogExpanded = ({ params, models }) => {
const expandedStr = JSON.stringify(models.expandedItems.value);
const useTreeViewLogExpanded = ({ params, store }) => {
const expandedStr = JSON.stringify(store.value.expansion.expandedItems);

React.useEffect(() => {
if (params.areLogsEnabled && params.logMessage) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ type TreeViewLogExpandedSignature = TreeViewPluginSignature<{

const useTreeViewLogExpanded: TreeViewPlugin<TreeViewLogExpandedSignature> = ({
params,
models,
store,
}) => {
const expandedStr = JSON.stringify(models.expandedItems.value);
const expandedStr = JSON.stringify(store.value.expansion.expandedItems);

React.useEffect(() => {
if (params.areLogsEnabled && params.logMessage) {
Expand Down
88 changes: 16 additions & 72 deletions docs/data/tree-view/rich-tree-view/headless/headless.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,94 +61,38 @@ useCustomPlugin.getDefaultizedParams = ({ params }) => ({
});
```

### Controllable models

A model is a value that can either be controlled or initialized using a prop.
The Tree View contains several models like the `expandedItems` model which contains the ids of the items currently expanded.

You can create your own models using the `models` property of your plugin:

```ts
useCustomPlugin.params = {
// ...other params
defaultCustomModel: true,
customModel: true,
};

useCustomPlugin.getDefaultizedParams = ({ params }) => ({
...params,
// ... other defaultized params
defaultCustomModel: params.defaultCustomModel ?? false,
});

useCustomPlugin.models = {
customModel: {
getDefaultValue: (params) => defaultCustomModel,
},
};
```

:::info
When creating a model, you should always set a default value to your `defaultCustomModel` prop. This will initialize the component's internal state when the model is uncontrolled.
See the [Params default value](/x/react-tree-view/rich-tree-view/headless/#params-default-value) section for more information.
:::

You can then use this model in your plugin (or in any other plugin) using the `models` parameter:

```ts
const useCustomPlugin = ({ models }) => {
React.useEffect(() => {
console.log(models.customModel.value);
});

const updateCustomModel = (newValue) =>
models.customModel.setControlledValue(newValue);

return {};
};
```

### Use elements from another plugin

Your plugin can access the instance methods, the params and the models of any other plugin.
Your plugin can access the instance methods, the params and the state of any other plugin.

```ts
const useCustomPlugin = ({ models }) => {
const useCustomPlugin = ({ store }) => {
const handleSomeAction = () => {
// Log the id of the items currently expanded
console.log(models.expandedItems.value);

// Collapse all the items
models.expandedItems.setValue([]);
console.log(store.value.expansion.expandedItems);

// Check if an item is expanded
const isExpanded = useSelector(selectorIsItemExpanded, 'some-item-id');
};
};
```

:::warning
Setting a model value in another plugin is wrong because it won't publish the associated callback.
We probably need a new abstraction here so that a plugin is always responsible for its model updates.
:::

### Define a plugin state

TODO

:::warning
Once `focusedItemId` becomes a model, we could consider removing the notion of state and just let each plugin define its state and provide methods in the instance to access / update it.
:::

### Populate the Tree View instance

The Tree View instance is an object accessible in all the plugins and in the Tree Item.
It is the main way a plugin can provide features to the rest of the component.

```ts
const useCustomPlugin = ({ models }) => {
const useCustomPlugin = ({ store }) => {
const toggleCustomModel = () =>
models.customModel.setValue(!models.customModel.value);
store.update((prevState) => ({
...prevState,
customModel: !prevState.customModel,
}));

return {
instance: {
Expand All @@ -161,7 +105,7 @@ const useCustomPlugin = ({ models }) => {
You can then use this instance method in any other plugin:

```ts
const useOtherCustomPlugin = ({ models, instance }) => {
const useOtherCustomPlugin = ({ instance }) => {
const handleSomeAction = () => {
instance.toggleCustomModel();
};
Expand All @@ -171,10 +115,13 @@ const useOtherCustomPlugin = ({ models, instance }) => {
### Emit and receive events

```ts
const useCustomPlugin = () => {
const useCustomPlugin = ({ store }) => {
const toggleCustomModel = () => {
const newValue = !models.customModel.value;
models.customModel.setValue(newValue);
const newValue = !selectorCustomModel(store.value);
store.update((prevState) => ({
...prevState,
customModel: newValue,
}));
publishTreeViewEvent(instance, 'toggleCustomModel', { value: newValue });
};

Expand Down Expand Up @@ -279,8 +226,6 @@ type UseCustomPluginSignature = TreeViewPluginSignature<{
slots: UseCustomPluginSlots;
// The slot props used by this plugin
slotProps: UseCustomPluginSlotProps;
// The name of the models defined by your plugin
modelNames: UseCustomPluginModelNames;
// The plugins this plugin needs to work correctly
dependencies: UseCustomPluginDependantPlugins;
}>;
Expand Down Expand Up @@ -314,8 +259,7 @@ type UseCustomPluginSignature = TreeViewPluginSignature<{
};
};
contextValue: { customPlugin: { enabled: boolean } };
modelNames: 'customModel';
// We want to have access to the expansion models and methods of the expansion plugin.
// We want to have access to the expansion state and methods of the expansion plugin.
dependencies: [UseTreeViewExpansionSignature];
}>;
```
Expand Down
29 changes: 1 addition & 28 deletions packages/x-tree-view/src/internals/models/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { EventHandlers } from '@mui/utils/types';
import { TreeViewExperimentalFeatures, TreeViewInstance, TreeViewModel } from './treeView';
import { TreeViewExperimentalFeatures, TreeViewInstance } from './treeView';
import type { MergeSignaturesProperty, OptionalIfEmpty } from './helpers';
import { TreeViewEventLookupElement } from './events';
import type { TreeViewCorePluginSignatures } from '../corePlugins';
Expand All @@ -14,20 +14,11 @@ export interface TreeViewPluginOptions<TSignature extends TreeViewAnyPluginSigna
slots: TSignature['slots'];
slotProps: TSignature['slotProps'];
experimentalFeatures: TreeViewUsedExperimentalFeatures<TSignature>;
models: TreeViewUsedModels<TSignature>;
store: TreeViewUsedStore<TSignature>;
rootRef: React.RefObject<HTMLUListElement | null>;
plugins: TreeViewPlugin<TreeViewAnyPluginSignature>[];
}

type TreeViewModelsInitializer<TSignature extends TreeViewAnyPluginSignature> = {
[TControlled in keyof TSignature['models']]: {
getDefaultValue: (
params: TSignature['defaultizedParams'],
) => Exclude<TSignature['defaultizedParams'][TControlled], undefined>;
};
};

type TreeViewResponse<TSignature extends TreeViewAnyPluginSignature> = {
getRootProps?: <TOther extends EventHandlers = {}>(
otherHandlers: TOther,
Expand All @@ -48,7 +39,6 @@ export type TreeViewPluginSignature<
contextValue?: {};
slots?: { [key in keyof T['slots']]: React.ElementType };
slotProps?: { [key in keyof T['slotProps']]: {} | (() => {}) };
modelNames?: keyof T['defaultizedParams'];
experimentalFeatures?: string;
dependencies?: readonly TreeViewAnyPluginSignature[];
optionalDependencies?: readonly TreeViewAnyPluginSignature[];
Expand All @@ -64,13 +54,6 @@ export type TreeViewPluginSignature<
contextValue: T extends { contextValue: {} } ? T['contextValue'] : {};
slots: T extends { slots: {} } ? T['slots'] : {};
slotProps: T extends { slotProps: {} } ? T['slotProps'] : {};
models: T extends { defaultizedParams: {}; modelNames: keyof T['defaultizedParams'] }
? {
[TControlled in T['modelNames']]-?: TreeViewModel<
Exclude<T['defaultizedParams'][TControlled], undefined>
>;
}
: {};
experimentalFeatures: T extends { experimentalFeatures: string }
? { [key in T['experimentalFeatures']]?: boolean }
: {};
Expand All @@ -92,7 +75,6 @@ export type TreeViewAnyPluginSignature = {
contextValue: any;
slots: any;
slotProps: any;
models: any;
experimentalFeatures: any;
publicAPI: any;
};
Expand Down Expand Up @@ -133,14 +115,6 @@ type TreeViewUsedExperimentalFeatures<TSignature extends TreeViewAnyPluginSignat
TSignature['optionalDependencies']
>;

type RemoveSetValue<Models extends Record<string, TreeViewModel<any>>> = {
[K in keyof Models]: Omit<Models[K], 'setValue'>;
};

export type TreeViewUsedModels<TSignature extends TreeViewAnyPluginSignature> =
TSignature['models'] &
RemoveSetValue<MergeSignaturesProperty<TreeViewRequiredPlugins<TSignature>, 'models'>>;

export type TreeViewUsedEvents<TSignature extends TreeViewAnyPluginSignature> =
TSignature['events'] & MergeSignaturesProperty<TreeViewRequiredPlugins<TSignature>, 'events'>;

Expand All @@ -161,7 +135,6 @@ export type TreeViewPlugin<TSignature extends TreeViewAnyPluginSignature> = {
}) => TSignature['defaultizedParams'];
getInitialState?: (params: TreeViewUsedDefaultizedParams<TSignature>) => TSignature['state'];
getInitialCache?: () => TSignature['cache'];
models?: TreeViewModelsInitializer<TSignature>;
params: Record<keyof TSignature['params'], true>;
itemPlugin?: TreeViewItemPlugin;
/**
Expand Down
6 changes: 0 additions & 6 deletions packages/x-tree-view/src/internals/models/treeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@ export interface TreeViewItemMeta {
label?: string;
}

export interface TreeViewModel<TValue> {
name: string;
value: TValue;
setControlledValue: (value: TValue | ((prevValue: TValue) => TValue)) => void;
}

export type TreeViewInstance<
TSignatures extends readonly TreeViewAnyPluginSignature[],
TOptionalSignatures extends readonly TreeViewAnyPluginSignature[] = [],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,43 @@
import { TreeViewItemId } from '../../../models';
import { createSelector, TreeViewRootSelector } from '../../utils/selectors';
import { selectorItemMeta } from '../useTreeViewItems/useTreeViewItems.selectors';
import { UseTreeViewExpansionSignature } from './useTreeViewExpansion.types';

const selectorExpansion: TreeViewRootSelector<UseTreeViewExpansionSignature> = (state) =>
state.expansion;

/**
* Get the expanded items.
* @param {TreeViewState<[UseTreeViewExpansionSignature]>} state The state of the tree view.
* @returns {TreeViewItemId[]} The expanded items.
*/
export const selectorExpandedItems = createSelector(
[selectorExpansion],
(expansionState) => expansionState.expandedItems,
);

/**
* Get the expanded items as a Map.
* @param {TreeViewState<[UseTreeViewExpansionSignature]>} state The state of the tree view.
* @returns {TreeViewExpansionValue} The expanded items as a Map.
*/
export const selectorExpandedItemsMap = createSelector([selectorExpandedItems], (expandedItems) => {
const expandedItemsMap = new Map<TreeViewItemId, true>();
expandedItems.forEach((id) => {
expandedItemsMap.set(id, true);
});

return expandedItemsMap;
});

/**
* Check if an item is expanded.
* @param {TreeViewState<[UseTreeViewExpansionSignature]>} state The state of the tree view.
* @returns {boolean} `true` if the item is expanded, `false` otherwise.
*/
export const selectorIsItemExpanded = createSelector(
[selectorExpansion, (_, itemId: string) => itemId],
(expansionState, itemId) => expansionState.expandedItemsMap.has(itemId),
[selectorExpandedItemsMap, (_, itemId: string) => itemId],
(expandedItemsMap, itemId) => expandedItemsMap.has(itemId),
);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ describeTreeView<[UseTreeViewExpansionSignature]>('useTreeViewExpansion plugin',
expect(view.isItemExpanded('1')).to.equal(true);
expect(view.isItemExpanded('2')).to.equal(false);
}).toErrorDev(
'MUI X: A component is changing the default expandedItems state of an uncontrolled TreeView after being initialized. To suppress this warning opt to use a controlled TreeView.',
'MUI X: A component is changing the default expandedItems state of an uncontrolled Tree View after being initialized. To suppress this warning opt to use a controlled Tree View.',
);
});
});
Expand Down
Loading