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

[CSS-in-JS] Add logicalShorthandCSS util + lint rule #6429

Merged
merged 11 commits into from
Nov 30, 2022

Conversation

cee-chen
Copy link
Member

@cee-chen cee-chen commented Nov 22, 2022

Summary

Unfortunately, shorthand properties that describe boxes such as margin and padding do not currently automatically respond to logical changes in display direction (see w3c/csswg-drafts#1282).

Until an official spec is created by W3C, this utility is a stopgap measure to convert (e.g.) margin shorthands to margin-inline and margin-block respectively.

Example usage within Emotion (intentionally similar in usage to logicalCSS()):

margin: 0 ${euiTheme.size.s};

becomes

${logicalShorthandCSS('margin', `0 ${euiTheme.size.s}`)}

I've also added a lint rule for this, which flags shorthand usage with multiple values. Single values (same value applied to all 4 sides) do not need to be converted to a logical property, and both the lint rule and utility intelligently detect this.

Note: there is no --fix behavior currently as I deemed it too unnecessarily complex to try and automate a fix - devs will have to manually import and call logicalShorthandCSS. Should W3C implement a single-line shorthand keyword or property, this will become much easier to do at that point, so I would strongly suggest waiting for that.

I strongly recommend following along by commit with whitespace changes disabled.

QA

Checked the following components for CSS output and visual smoke test:

  • EuiBadge
  • EuiHeaderBreadcrumbs
  • EuiButton
  • EuiCode
  • EuiExpression
  • EuiListGroupItem
  • EuiLoadingSpinner
  • EuiMarkdownFormat
  • EuiModalHeader
  • EuiPopoverFooter / EuiPopoverTitle
  • EuiText
  • EuiToken
  • EuiTooltip (arrow)

General checklist

Comment on lines +16 to +27
// Construct a regex to find shorthands with more than one value
// Shorthands with only 1 value set (applied to all 4 sides) are fine
const logicalShorthandsRegexes = _shorthands.map((property) => {
const start = `${property}:\\s*`; // e.g. `margin: `
const interpolatedVar = '\\${[^}]+}'; // e.g. `${euiTheme.something}`
const staticValue = '[^\\s(!};]'; // e.g. `20px` - any value that doesn't have a space, !keyword, } interpolation, or semicolon
const value = `(${interpolatedVar}|${staticValue})+`; // Values can be variable or static
const end = ';'; // Ensure that the match ends with a semicolon

return `(${start}(${value}\\s+)+${value}${end})`; // Only match multiple values (spaces between values)
});
const logicalShorthandsRegex = logicalShorthandsRegexes.join('|');
Copy link
Member Author

@cee-chen cee-chen Nov 22, 2022

Choose a reason for hiding this comment

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

Just want to mention a couple things: I'm aware this is a moderately insane regex, so apologies for that. I tried to make it easier to read by splitting up the various parts but I'm not 100% sure I succeeded 🥲

I will say breaking it up like this does make it a little difficult to easily copy/paste into (e.g.) an online regex tester. For future me's records, my recommendation is doing this:

  1. Add console.log(logicalShorthandsRegex); below line 27
  2. Run yarn jest scripts/eslint-plugin/css_logical_properties.test.js and copy the console log output
  3. Paste just one property regex into your regex tool (rather than all joined regexes), like so:
  4. Debug/play around with the various portions accordingly to try and solve whatever false positive or negative may be occurring
  5. Update Jest unit tests after to ensure said false positive/negative is or isn't caught by the linter

Comment on lines -45 to +65
const templateContents = node.quasis || [];
templateContents.forEach((quasi) => {
const stringLiteral = quasi?.value?.raw;
if (!stringLiteral) return;
const stringLiteral = context.getSourceCode().getText(node);
const content = stringLiteral.replace(/^`|`$/g, ''); // Strip starting/ending backtick
Copy link
Member Author

@cee-chen cee-chen Nov 22, 2022

Choose a reason for hiding this comment

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

Just want to quickly highlight why this change to context.getSourceCode().getText(node) (docs) was necessary:

If getText() isn't used, AST unfortunately breaks up node.quasis between each ${interpolatedVar} usage, meaning a basic padding: 0 ${euiTheme.size.s}; line isn't correctly caught/linted.

Here's an example of how AST breaks down template literal quasis which you can see yourself via AST Explorer:

For the purposes of our lint rule for EUI, since we don't necessarily care what the var interpolates to[1], just that we want to check if there's more than one var, so getting the flat/non-interpolated text is the solution. It also incidentally saves us from another forEach iteration so is possibly more performant in any case.

[1] This is because our vars are generally pretty safe... although if someone does something like const foo = '0 1 2 3'; padding: ${foo}; our linter would not catch this 🤔 Hopefully we just don't do that kind of shenanigans though.

@kibanamachine
Copy link

Preview documentation changes for this PR: https://eui.elastic.co/pr_6429/

1 similar comment
@kibanamachine
Copy link

Preview documentation changes for this PR: https://eui.elastic.co/pr_6429/

@cee-chen cee-chen force-pushed the logicals branch 2 times, most recently from 929b747 to 7611e97 Compare November 23, 2022 23:07
@cee-chen
Copy link
Member Author

While QAing the updated components, I realized that my logical CSS properties for border-color/width/style were not quite correct. Also, border-radius is a ludicrously special case due to the fact that it addresses corners, not edges (and thus has no -inline/-block properties), and additionally has slash syntax. So I did what any insane person would do and wrote way too much overengineered code to handle an edge case we don't actually have in EUI because I was having fun. Please do not judge me.

- which automatically handles converting CSS shorthands for 4 box sides to 2 separate logical properties

- see codeblock comment for more info - a single property/keyword does not yet exist, but when one is added the CSS spec we can flip this util over
- single values are ok; no need to make them logical if they're the same on all sides

- no `fix` option for this sadly, since the extra logic would be overly complex (either have to handle newlines/indentations, or have to handle a util import, both of which feel overly complex)

- note: `context.getSourceCode().getText(node)` is needed here over iterating through quasi's, because AST breaks up the template literal by vars found in within the literal, whereas we really only want to evaluate the exact text of variable usage itself
- this actually also saves us from another iteration forEach so is unintentionally a slight perf boost(?)
- requires a slightly different syntax from margin/padding/inset
me: i hate math

also me: i'm gonna spend several hours doing what basically amounts to light geometry and matrices
- noticed while grepping for shorthand properties
- was triggering lint rule
+ fix other example title
@kibanamachine
Copy link

Preview documentation changes for this PR: https://eui.elastic.co/pr_6429/

@elizabetdev elizabetdev requested review from elizabetdev and removed request for elizabetdev November 28, 2022 13:41
Copy link
Contributor

@breehall breehall left a comment

Choose a reason for hiding this comment

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

Thank you for adding the background into this PR. Out of curiosity, was this broken and we didn't realize it?

I pulled the branch and played around the the regex that was created in an online editor. The regex is crazy, but your solution is very elegant!

With the introduction of logicalBorderRadiusCSS, should we add some documentation to the Borders page like we did with Size?

Comment on lines +42 to +59
switch (values.length) {
case 1:
// If it's the same value all around, no need to use logical properties
return `${property}: ${value};`;
case 2:
verticalBlockValue = values[0];
horizontalInlineValue = values[1];
break;
case 3:
verticalBlockValue = `${values[0]} ${values[2]}`;
horizontalInlineValue = values[1];
break;
case 4:
default:
verticalBlockValue = `${values[0]} ${values[2]}`;
horizontalInlineValue = `${values[3]} ${values[1]}`; // Note: left (4th value) comes before right (2nd value)
break;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Love this. It feels very self-explanatory when reading.

Copy link
Member Author

@cee-chen cee-chen Nov 30, 2022

Choose a reason for hiding this comment

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

Woohoo, very satisfying to know! Also just to quickly address one of your top level q's in this thread:

Out of curiosity, was this broken and we didn't realize it?

Our logical CSS work has been more of a speculative enhancement. There isn't (yet) a specific need to support direction or writing-mode in Kibana or Elastic, but since it's easier for EUI to set the standard and other apps and teams follow, we're kind of making it up as we go.

With the introduction of logicalBorderRadiusCSS, should we add some documentation to the Borders page like we did with Size?

The logicalShorthandCSS actually supports border radius (just without the ignoreZeroes) flag, so I didn't know if I needed extra documentation for this - I mostly wanted the ignoreZeroes option for our own EUI css where we know we're not overriding existing border-radius 🤔

I'll think about this more and circle back on it! To be honest, I'm hoping we can eventually remove that special logic totally if W3 comes out with a more handy CSS spec where the API remains the same except for a keyword.

Comment on lines +18 to +26
const logicalShorthandsRegexes = _shorthands.map((property) => {
const start = `${property}:\\s*`; // e.g. `margin: `
const interpolatedVar = '\\${[^}]+}'; // e.g. `${euiTheme.something}`
const staticValue = '[^\\s(!};]'; // e.g. `20px` - any value that doesn't have a space, !keyword, } interpolation, or semicolon
const value = `(${interpolatedVar}|${staticValue})+`; // Values can be variable or static
const end = ';'; // Ensure that the match ends with a semicolon

return `(${start}(${value}\\s+)+${value}${end})`; // Only match multiple values (spaces between values)
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🧠 Played around with the margin and scroll-margin regex patterns in an online playground and tried to come up with various patterns. From what I tested, the regex seems solid to me!

Copy link
Member Author

Choose a reason for hiding this comment

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

Woohoo! I'm sure it's not bulletproof, but it should hopefully capture 90%+ of common use cases, and we can continue to tweak (or line-disable if needed) it as we go on.

@cee-chen cee-chen merged commit 0c76929 into elastic:main Nov 30, 2022
@cee-chen cee-chen deleted the logicals branch November 30, 2022 22:49
jbudz pushed a commit to elastic/kibana that referenced this pull request Dec 19, 2022
eui@70.4.0 ⏩ eui@71.0.0

---

## [`71.0.0`](https://github.com/elastic/eui/tree/v71.0.0)

- Implemented new `EuiRange` and `EuiDualRange` designs where the
`levels` are now on top of the tracks
([#6092](elastic/eui#6092))
- Added `discuss` and `dotInCircle` glyphs to `EuiIcon`
([#6434](elastic/eui#6434))
- Added `article` glyph to `EuiIcon`
([#6437](elastic/eui#6437))
- Changed the `EuiProvider` usage warnings to not rely on development
mode. ([#6451](elastic/eui#6451))

**Breaking changes**

- `EuiDualRange` now explicitly requires both `min` and `max` via props
types, to match `EuiRange`
([#6092](elastic/eui#6092))
- `EuiRange` and `EuiDualRange`'s `compressed` size no longer impacts
track or level sizes, but continues to compress tick and input sizes.
([#6092](elastic/eui#6092))
- Removed all variables for the following components from EUI's theme
JSON files: ([#6443](elastic/eui#6443))
  - `euiCollapsibleNav*`
  - `euiColorPicker*`
  - `euiContextMenu*`
  - `euiControlBar*`
  - `euiDataGrid* `(except for z-indices and cell padding sizes)
  - `euiDatePicker*`
  - `euiSuperDatePicker*`
  - `euiDragAndDrop*`
  - `euiEuiEmptyPrompt*`
  - `euiFilePicker*`
  - `euiRange*`
  - `euiHeaderLinks*`
  - `euiKeyPad*`
  - `euiMarkdownEditor*`
  - `euiResizable*`
  - `euiSelectable*`
  - `euiSideNav*`
  - `euiStep*`
  - `euiSuggest*`
  - `euiTable*` (except for color variables)
  - `euiTooltip*`
- `euiButtonFontWeight`, `euiButtonDefaultTransparency`, and
`euiButtonMinWidth`
- If you were importing any of the above removed JSON variables, we
strongly recommend using generic color or sizing variables from
`useEuiTheme()` instead.
([#6443](elastic/eui#6443))

**CSS-in-JS conversions**

- Converted `EuiRange` and `EuiDualRange` to Emotion; Removed
`$euiRangeThumbRadius`
([#6092](elastic/eui#6092))
- Added a new `logicalStyles` utility that automatically converts all
non-logical properties in a `style` object to their corresponding
logical properties ([#6426](elastic/eui#6426))
- Added a new `logicalShorthandCSS` utility that automatically converts
`margin`, `padding`, and other 4-sided shorthands to their corresponding
logical properties ([#6429](elastic/eui#6429))
- Added a new `logicalBorderRadiusCSS` utility that automatically
converts `border-radius` to corresponding logical properties
([#6429](elastic/eui#6429))

Co-authored-by: Constance Chen <constance.chen@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants