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

ui-utils: Add new package with two type checking functions #28028

Closed
wants to merge 4 commits into from
Closed
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
6 changes: 6 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1817,6 +1817,12 @@
"markdown_source": "../packages/token-list/README.md",
"parent": "packages"
},
{
"title": "@wordpress/ui-utils",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ui-utils as a name doesn't make sense for me. These utilities can be useful for any code. Why not @wordpress/is

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this comment doesn't make sense if we decide to drop the package.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ui-utils as a name doesn't make sense for me.

The reason for this PR and for this package is to provide a space for us to migrate the other utilities found in the G2 Component system:

https://github.com/ItsJonQ/g2/tree/master/packages/utils/src

Why not @wordpress/is

I'm happy renaming it to @wordpress/utils package if that were the case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We used to have a @wordpress/utils a long time ago and we removed it in favor of smaller packages with clear purpose. A @wordpress/is would have been a good one here but IMO, lodash offers already all of these functions and since lodash is already used across our codebase, I'd just continue using it instead of replacing it with our custom lodash.

"slug": "packages-ui-utils",
"markdown_source": "../packages/ui-utils/README.md",
"parent": "packages"
},
{
"title": "@wordpress/url",
"slug": "packages-url",
Expand Down
4 changes: 4 additions & 0 deletions packages/compose/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### New Features

- Add `is` utility

## 3.4.0 (2019-06-12)

### New Features
Expand Down
1 change: 1 addition & 0 deletions packages/ui-utils/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
5 changes: 5 additions & 0 deletions packages/ui-utils/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- Learn how to maintain this file at https://github.com/WordPress/gutenberg/tree/master/packages#maintaining-changelogs. -->

## Unreleased

Initial release.
68 changes: 68 additions & 0 deletions packages/ui-utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# UI Utils

A collection of utilities useful for building user interfaces.

## API

<!-- START TOKEN(Autogenerated API docs) -->

<a name="isDefined" href="#isDefined">#</a> **isDefined**

Checks whether a value is defined, asserting that it is neither
null nor undefined.

_Parameters_

- _value_ `(T|undefined|null)`: The value to check.

_Returns_

- (unknown type): Whether the value is undefined.

<a name="isObjectInterpolation" href="#isObjectInterpolation">#</a> **isObjectInterpolation**

There's no way to detect whether an `any` type is an `ObjectInterpolation` because `ObjectInterpolation`s
are merely objects where the keys are either (1) [CSS property names](https://github.com/emotion-js/emotion/blob/a72e6dc0f326b7d3d6067698d433018ee8c4cbf1/packages/serialize/types/index.d.ts#L10),
(2) [CSS Pseudo property names](https://github.com/emotion-js/emotion/blob/a72e6dc0f326b7d3d6067698d433018ee8c4cbf1/packages/serialize/types/index.d.ts#L18),
or (3) [anything else where the value for the key is another interpolation](https://github.com/emotion-js/emotion/blob/a72e6dc0f326b7d3d6067698d433018ee8c4cbf1/packages/serialize/types/index.d.ts#L19).
That means ObjectInterpolations can look all of the following ways:

```js
const oi = {
color: 'black',
};

const oi = {
':disabled': css`color: black;`,
};

const oi = {
'@media (prefers-color-scheme: light)': css({
backgroundColor: 'black',
color: 'white',
}),
};
```

Therefore, this function only accepts `TemplateStringsArray` and `Interpolation` for it's arguments rather than `any`, which
cannot be refined to an `ObjectInterpolation`. However, given any generic `Interpolation`, we _can_ tell whether it is an
`ObjectInterpolation` specifically by simply checking whether the value is "plain object" as defined by `lodash`'s `isPlainObject`.

Note: The links above reference a specific commit for **Emotion 10**. Emotion 11 is out but we're stuck on Emotion 10 until we can
re-write `create-styles` and upgrade all of `@wordpress/components` to use the new version of Emotion.

At that point we'll have to revisit this function anyway as it seems that the `ObjectInterpolation` type no longer
exists and that the concept has evolved.

_Parameters_

- _value_ (unknown type): The value to check.

_Returns_

- (unknown type): Whether the value is an ObjectInterpolation.


<!-- END TOKEN(Autogenerated API docs) -->

<br/><br/><p align="center"><img src="https://s.w.org/style/images/codeispoetry.png?1" alt="Code is Poetry." /></p>
39 changes: 39 additions & 0 deletions packages/ui-utils/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@wordpress/ui-utils",
"version": "0.0.1",
"description": "A collection of simple utilities for building user interfaces.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
"keywords": [
"wordpress",
"gutenberg",
"utils"
],
"homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/ui-utils/README.md",
"repository": {
"type": "git",
"url": "https://github.com/WordPress/gutenberg.git",
"directory": "packages/ui-utils"
},
"bugs": {
"url": "https://github.com/WordPress/gutenberg/issues"
},
"files": [
"build",
"build-module",
"build-types",
"src",
"*.md"
],
"main": "build/index.js",
"module": "build-module/index.js",
"react-native": "src/index",
"types": "build-types",
"sideEffects": false,
"dependencies": {
"lodash": "^4.17.19"
},
"publishConfig": {
"access": "public"
}
}
2 changes: 2 additions & 0 deletions packages/ui-utils/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as isDefined } from './is-defined';
export { default as isObjectInterpolation } from './is-object-interpolation';
13 changes: 13 additions & 0 deletions packages/ui-utils/src/is-defined/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* eslint-disable jsdoc/valid-types */
/**
* Checks whether a value is defined, asserting that it is neither
* null nor undefined.
*
* @template T
* @param {T | undefined | null} value The value to check.
* @return {value is T} Whether the value is undefined.
*/
const isDefined = ( value ) => value !== null && value !== undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This already exists in lodash isNil

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, but if we'd like to remove lodash at scale, as described by #17025, we'd need to replace this utility (it's a quite useful one, if you search through G2 for is.defined you'll see it's used almost everywhere).

We could replace it with ! isNil but that doesn't accomplish getting rid of lodash at all.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I agree with "getting rid of lodash by adding our own utils". I think getting rid of lodash by using native functions is definitely a good thing but other than that:

  • Lodash can be replaced with lodash-es for npm consumers
  • Lodash is only loaded once in WordPress, it's better that bundling the same utils multiple times.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me. We'll replace is.defined with ! isNil everywhere then?


export default isDefined;
/* eslint-enable jsdoc/valid-types */
49 changes: 49 additions & 0 deletions packages/ui-utils/src/is-object-interpolation/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* global TemplateStringsArray */
/**
* External dependencies
*/
import isPlainObject from 'lodash/isPlainObject';

/* eslint-disable jsdoc/valid-types */
/**
*
* There's no way to detect whether an `any` type is an `ObjectInterpolation` because `ObjectInterpolation`s
* are merely objects where the keys are either (1) [CSS property names](https://github.com/emotion-js/emotion/blob/a72e6dc0f326b7d3d6067698d433018ee8c4cbf1/packages/serialize/types/index.d.ts#L10),
* (2) [CSS Pseudo property names](https://github.com/emotion-js/emotion/blob/a72e6dc0f326b7d3d6067698d433018ee8c4cbf1/packages/serialize/types/index.d.ts#L18),
* or (3) [anything else where the value for the key is another interpolation](https://github.com/emotion-js/emotion/blob/a72e6dc0f326b7d3d6067698d433018ee8c4cbf1/packages/serialize/types/index.d.ts#L19).
* That means ObjectInterpolations can look all of the following ways:
*
* ```js
* const oi = {
* color: 'black',
* };
*
* const oi = {
* ':disabled': css`color: black;`,
* };
*
* const oi = {
* '@media (prefers-color-scheme: light)': css({
* backgroundColor: 'black',
* color: 'white',
* }),
* };
* ```
*
* Therefore, this function only accepts `TemplateStringsArray` and `Interpolation` for it's arguments rather than `any`, which
* cannot be refined to an `ObjectInterpolation`. However, given any generic `Interpolation`, we _can_ tell whether it is an
* `ObjectInterpolation` specifically by simply checking whether the value is "plain object" as defined by `lodash`'s `isPlainObject`.
*
* Note: The links above reference a specific commit for **Emotion 10**. Emotion 11 is out but we're stuck on Emotion 10 until we can
* re-write `create-styles` and upgrade all of `@wordpress/components` to use the new version of Emotion.
*
* At that point we'll have to revisit this function anyway as it seems that the `ObjectInterpolation` type no longer
* exists and that the concept has evolved.
*
* @param {TemplateStringsArray | import('create-emotion').Interpolation} value The value to check.
* @return {value is import('create-emotion').ObjectInterpolation} Whether the value is an ObjectInterpolation.
*/
const isObjectInterpolation = ( value ) => isPlainObject( value );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just use isPlainObject directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usefulness of this function comes primarily from the type defintions. It's impossible to refine the type any other way other than through type definitions which TypeScript will trust implicitly. This allows the newly added packages to be fully and completely typed.

For example, given the following (real) code example:

/**
 * @param {TemplateStringsArray | import('create-emotion').Interpolation<undefined>} template
 * @param {import('create-emotion').Interpolation<undefined>[]} args The styles to compile.
 * @returns {ReturnType<compile>} The compiled styles.
 */
export function css(template, ...args) {
	if (isObjectInterpolation(template)) {
		return compile(responsive(template));
	}

	if (Array.isArray(template)) { // => TemplateStringsArray
		for (let i = 0, len = template.length; i < len; i++) {
			const n = template[i];
			if (isObjectInterpolation(n)) {
				template[i] = responsive(n);
			}
		}
		return compile(template, ...args);
	}

	return compile(template, ...args);
}

If you replace isObjectInterpolation with isPlainObject and responsive's first argument will not be refined enough to be accepted as responsive only accepts an ObjectInterpolation for it's first argument.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's a valid reason for us at least to add a new package to be honest. Can't this be solved by an explicit type cast instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, this package isn't complete as it stands, so it's justification can't live on just these two utilities. The issue linked in the PR description has a short list of utilities that would live in this package (and those are just the ones used by the Text and Truncate components, there are many more if you check out the G2 repo).

FWIW I hadn't tried to type cast it but it does work, so we can remove this utility if you'd prefer to just use type casts (that's all this function does anyhow).

Regardless, I'll open a separate PR with a different utility adding the same ui-utils package for getOptimalTextShade, getDisplayName, and getComputedColor as the issue linked in the PR describes. This was just meant as an incremental way of doing it without moving more code at one time than is possible for a person to effectively review.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think adding packages shouldn't be seen as something we should do easily especially in the context of WordPress, so I'd personally feel better if we start by doing inline utilities in the "components" package and discuss whether it deserves a separate package or not as the usage grows.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about utilities used by mulitple packages like getDisplayName? Where should that live? We can't introduce them in components as that would create a circular dependency. We can duplicate the code in the packages if you'd prefer that. Or we can export them from an unrelated package, but then that package will be responsible for something that it shouldn't care about.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does getDisplayName do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/**
 * Gets the displayName of a React Component or element.
 *
 * @param {string | import('react').ComponentType} tagName
 *
 * @return {string} The display name of the Component / tagName.
 */
export function getDisplayName(tagName) {
	const displayName =
		typeof tagName === 'string'
			? tagName
			: tagName.displayName || tagName.name || 'Component';

	return displayName;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So looking a g2. It seems to be only used once. That said it seems to be used to compute components display name? In fact we have something close to it in createHigherOrderComponent in @wordpress/compose so it does seem like a natural place for it if its usage grows.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you're right, I thought context used it as well but I was wrong. We'll inline that one too I guess.

/* eslint-enable jsdoc/valid-types */

export default isObjectInterpolation;