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

refactor: #136 TextArea - Add CSS resizing #150

Merged
merged 3 commits into from
Aug 12, 2024

Conversation

kurtdoherty
Copy link
Contributor

@kurtdoherty kurtdoherty commented Aug 1, 2024

Context

  • v5 component refactor: TextArea #136 describes updates needed for the TextArea component to align with the Design System;
  • The key breaking change is that the TextArea can no longer be manually resized; rather, it should grow/shrink based on the amount of content entered.
  • I am leveraging an experimental CSS property, field-sizing to achieve the resizing behaviour for CSS-only and React consumers on Chrome and Edge browsers, with a fallback to a JS-based resizing solution for React-only consumers on other browsers.
  • This work is split across two PRs:

This PR (Part 1)

  • Moves __styles__/index.ts to styles.ts per the project structure conventions;
  • Updates CSS for ElTextArea styled component to:
    • Use the newly available design tokens
    • Expand the invalid state styling too be used either when either the elTextAreaHasError class name is applied, or the :invalid or :user-invalid pseudo-classes are active;
    • Provide styling for :disabled and :read-only states;
    • Provide max and min height constraints based on CSS variables that CSS-only consumers can easily override;
  • Updates ElTextArea to leverage experimental field-sizing property if supported;
  • Updates Storybook docs to demonstrate basic usage, invalid state, and resizing behaviour;
  • Updates TextArea and its prop interface to include minRows, maxRows and rows.

textarea p1

note: The design guidelines have not be finalised for this component, so there may be some additional changes once they are, particularly the treatment of default min/max rows values or the TextArea being resizable by default as opposed to fixed size by default.

note: This PR is on the large size, so if you'd like me to split it up further I can. In hindsight, the Storybook updates could have been left until later 😅

@kurtdoherty kurtdoherty added the v5 Issues for v5 release label Aug 1, 2024
@kurtdoherty kurtdoherty force-pushed the 136-v5-component-refactor-textarea branch from a81340c to a1ece07 Compare August 1, 2024 22:09
@@ -77,7 +77,7 @@ export const BasicForm = {
</InputWrap>
<InputWrapFull>
<InputGroup>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: type is no longer an accepted prop for TextArea, so fixing up these stories.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, agreed, there's no need

@@ -2,7 +2,7 @@ import { styled } from '@linaria/react'
import { ElIcon } from '../../icon/__styles__'
import { ElLabel } from '../../label/__styles__'
import { ElInput, elHasInputError } from '../../input/__styles__'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: Fixing up to import from the root textarea folder instead of relying on internal file structures.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: Moved to textarea/styles.ts

Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: The current approach of snapshotting the render result is not ideal. It means we are capturing a host of React Testing Library's implementation details which are irrelevant to our component unit tests. As you'll see in the unit test file, I've updated the unit tests to snapshot the document fragment that contains our rendered component.

aside: It may be helpful to add this as a task to all the v5 refactor issues so that devs can remember to update the tests.

@@ -1,9 +1,19 @@
import { render } from '@testing-library/react'
import { render, screen } from '@testing-library/react'
import { TextArea } from '..'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: I've updated the unit tests to avoid the generic "should match a snapshot". While this kind of unit test provides some value, it's missing out (IMO) on one of the most important aspects of unit tests: documentation. As such, I've replaced it with three tests that cover some of the important aspects we would want to document about our TextArea, though one is currently skipped (see code comment).

aside: I would not say that these unit tests are hitting the bar of "necessary and sufficient" by themselves, but the addition of the visual snapshots helps get us pretty close as they provide coverage of the invalid, disabled and read-only states, plus the initial sizing of text areas based on the minRows, maxRows and rows props.

I can provide more info on what the terms "necessary and sufficient" mean separately (they are terms we recently introduced to the Console FE team).

Copy link
Contributor

Choose a reason for hiding this comment

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

If you have some docs on this, would be more than happy to add to the wiki

'--textarea-min-rows': number
}

export const ElTextArea = styled.textarea`
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: The code comment below highlights an important decision I made in the implementation of this component. Instead of relying on Linaria's ability to convert ${({ maxRows }) => ...} into a randomly named CSS variable for us, I've opted to handle it manually in order to provide well-named CSS variables that CSS-only consumers can leverage.

I would have preferred to rely on data attributes on the textarea, but there's currently no way for CSS to use the value of a data attribute. See attr() for more information.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, makes sense - appreciate the comments in here too

// a friendly interface for CSS-only consumers.
--textarea-max-rows: infinity;
--textarea-min-rows: 2;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: The code comment below is another important decision. While I strongly believe there should be zero developer-defined, component-specific design tokens in Elements, I don't think that should prevent us from leveraging CSS variables in a "private" manner to aide the definition of our styles.

In order to try and communicate they are "private"/"implementation detail" CSS variables, I've prefixed them with the standard double underscore, __. Not sure if that's something we want to do or not.

If we'd prefer not to do this at all, I can remove these and just use the real design tokens directly in all the places they're referenced.

Copy link
Contributor

Choose a reason for hiding this comment

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

It does look a bit odd syntactically but the comment explains fine. I would suggest as long as they are outside the root scope it's probably fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@willmcvay I agree the syntax looks weird. We could choose to use private as a prefix? i.e. --private-textarea-border-width 🤔

@kurtdoherty kurtdoherty force-pushed the 136-v5-component-refactor-textarea branch from a1ece07 to c7fdf7f Compare August 2, 2024 00:03
@supports (field-sizing: content) {
field-sizing: content;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: The code comment below highlights an important design decision I've made with the max and min height constraints for the text area. Since we want CSS-only consumers to get the correct resizing behaviour, we need to facilitate the max/min constraints in CSS, not JS.

I've used the min and max block-size CSS properties as they account for the consumer's writing-mode.

@kurtdoherty kurtdoherty force-pushed the 136-v5-component-refactor-textarea branch from c7fdf7f to 8db308a Compare August 2, 2024 00:18
// @see https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode
// @see https://developer.mozilla.org/en-US/docs/Web/CSS/max-block-size
// @see https://developer.mozilla.org/en-US/docs/Web/CSS/min-block-size
max-block-size: calc(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: We use the new lh unit to calculate the length of the rows, then add on the padding and border. See MDN's docs on CSS values and units for more information on lh, but the TL;DR is:

lh and rlh are relative lengths units similar to em and rem. The difference between lh and rlh is that the first one is relative to the line height of the element itself, while the second one is relative to the line height of the root element, usually <html>.

Copy link
Contributor

Choose a reason for hiding this comment

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

I did just check this out in Safari just because Safari and couldn't see any issues FYI

outline: none;
border-color: var(--outline-text_input-focus);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: the original Elements text area did not have styles for disabled or read-only states. These have been added here. The Design System does not currently differentiate visually between disable and read-only; this may change in future.

&:disabled {
background-color: var(--fill-default-lightest);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: The original text area only applied the invalid state styles when the elTextAreaHasError class was present. This has been expanded to include the :invalid and :user-invalid pseudo-classes.

Copy link
Contributor Author

@kurtdoherty kurtdoherty Aug 2, 2024

Choose a reason for hiding this comment

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

note: Some minor updates here to include the extra stories that have been added.

aside: I'm keen to understand why Elements prefers MDX files over Storybook's autogenerated DocsPage functionality. What I'm doing here (e.g. the use of the Description component to render the JS Doc comments from the component or a specific story) is all handled by Storybook's DocsPage feature, so I'm not sure what value we are getting from doing this ourselves 🤔 Is it primarily to facilitate the RenderHtmlMarkup component?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: I've expanded the number of stories being demonstrated here, as well as applied Storybook's types to the exports. The JS Doc for each story is picked up by Storybook's Description component, which is now in use in textarea.mdx.


// NOTE: We omit the `cols` prop because our text area should always grow to the width of its container.
interface RestrictedTextareaHTMLAttributes extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'cols'> {}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: I've updated how the component's prop interface is defined for two reasons:

  1. To add the new minRows and maxRows props; and,
  2. To use the TextareaHTMLAttributes instead of InputHTMLAttributes (the latter one is for <input> elements).

* resizing behaviour is available for CSS-only consumers on Chrome and Edge. For browsers that do not yet
* support the [field-sizing](https://developer.mozilla.org/en-US/docs/Web/CSS/field-sizing) property, we
* fallback to a JS-based resizing solution that is only available to React-based consumers.
*/
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: I've opted to use forwardRef's generic type slots instead of the original TextAreaWrapped type. This makes for a simpler approach and ensures our forwarded ref is correctly typed.

// NOTE: `rows` takes precedence. If it's defined, the text area is effectively fixed-height.
// If it's not defined, we allow resizing to occur between the min and max rows.
'--textarea-max-rows': rows ?? maxRows,
'--textarea-min-rows': rows ?? minRows,
Copy link
Contributor Author

@kurtdoherty kurtdoherty Aug 2, 2024

Choose a reason for hiding this comment

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

note: I'm using satisfies TextAreaCSSProperties (a type which comes from our styles.ts module) in order to ensure we don't provide more styles here than what we explicitly permit through the TextAreaCSSProperties.

I then assert this to be the CSSProperties type in order to satisfy React/Linaria's expectations (they don't consider the CSS variables to be valid property names).

@kurtdoherty kurtdoherty marked this pull request as ready for review August 2, 2024 00:44
@kurtdoherty kurtdoherty changed the title refactor: #136 TextArea (part 1) refactor: #136 TextArea - Add CSS resizing Aug 2, 2024
Copy link
Contributor

@willmcvay willmcvay left a comment

Choose a reason for hiding this comment

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

Hi @kurtdoherty so looks good to me - have tested locally in Safari, FF & Chromium because of some of the edgier properties and works as expected which is great - will check out the JS fallback next.

Comments are appreciated for the more non-vanilla implementations - I appreciate, it's a tricky bit of code.

Just that small issue with the stories file to move to the new structure in SB but happy if you want to move that to the second PR.

@@ -77,7 +77,7 @@ export const BasicForm = {
</InputWrap>
<InputWrapFull>
<InputGroup>
Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, agreed, there's no need

"rerender": [Function],
"unmount": [Function],
}
exports[`can set custom min/max rows 1`] = `
Copy link
Contributor

Choose a reason for hiding this comment

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

Much cleaner without the React internals :-)

@@ -1,9 +1,19 @@
import { render } from '@testing-library/react'
import { render, screen } from '@testing-library/react'
import { TextArea } from '..'

Copy link
Contributor

Choose a reason for hiding this comment

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

If you have some docs on this, would be more than happy to add to the wiki

'--textarea-min-rows': number
}

export const ElTextArea = styled.textarea`
Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, makes sense - appreciate the comments in here too

// a friendly interface for CSS-only consumers.
--textarea-max-rows: infinity;
--textarea-min-rows: 2;

Copy link
Contributor

Choose a reason for hiding this comment

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

It does look a bit odd syntactically but the comment explains fine. I would suggest as long as they are outside the root scope it's probably fine.

// @see https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode
// @see https://developer.mozilla.org/en-US/docs/Web/CSS/max-block-size
// @see https://developer.mozilla.org/en-US/docs/Web/CSS/min-block-size
max-block-size: calc(
Copy link
Contributor

Choose a reason for hiding this comment

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

I did just check this out in Safari just because Safari and couldn't see any issues FYI

src/components/textarea/textarea.stories.tsx Outdated Show resolved Hide resolved
@adrian-reapit
Copy link
Contributor

With the other PR this is quite an update on a normal TextArea and if its got bespoke 'tricky' code it might better to make a new component. MaterialUI has a TextareaAutosize component, name makes clear what you are getting https://mui.com/base-ui/react-textarea-autosize/

@kurtdoherty
Copy link
Contributor Author

kurtdoherty commented Aug 5, 2024

With the other PR this is quite an update on a normal TextArea and if its got bespoke 'tricky' code it might better to make a new component. MaterialUI has a TextareaAutosize component, name makes clear what you are getting https://mui.com/base-ui/react-textarea-autosize/

@adrian-reapit I did consider that option originally, but decided to take the approach I've presented in these PRs because:

  • We've previously discussed as a DS working group that we want to aim for a 1-1 equivalency between Elements and the Design System. While we shouldn't blindly adhere to that in every case, I do think it makes sense for TextArea;
  • The Design System has no desire for a text area that can be manually resized. They will either be fixed height or will automatically resize between a min and max number of lines;
  • I view the JS-based fallback for resizing to be (relatively) short-lived. Once field-sizing support is rolled out for Safari (it's already in progress) and Firefox (its on their radar), all the "tricky" code will disappear;
  • Once the JS-based resizing logic is removed, the idea of having two text area components whose only difference is the field-sizing CSS property doesn't seem ideal; and,
  • I really value the DX of seeing a design use a TextArea component and being able to reach for a component with the same name and the same props.

@willmcvay
Copy link
Contributor

@kurtdoherty @adrian-reapit Just to resolve the slight log-jam, why not take the "Styles Only" usage approach and export a wrapped version of ElTextArea like we do with the Accordion atoms. Then people can choose between batteries included or simple HTML eg https://github.com/reapit/elements/blob/main/src/components/accordion/accordion.atoms.tsx#L12

@kurtdoherty
Copy link
Contributor Author

kurtdoherty commented Aug 6, 2024

@willmcvay we already are exporting the basic styled component, ElTextArea. You can see this happening in TextArea/styles.ts and TextArea/index.ts. 👍

I'm not sure there's much value in exporting another component that simply wraps it? The atom would want to be called TextArea, but that's the name of the "main" component too. This isn't a problem with the Accordion atoms because they're all uniquely named 🤔

@kurtdoherty
Copy link
Contributor Author

note: I have a meeting with Andrei today to talk through the component interface, particularly around what design would like to see as the properties for their TextArea in Figma and how that works for us, so there may be some minor changes land after that conversation.

@willmcvay
Copy link
Contributor

@willmcvay we already are exporting the basic styled component, ElTextArea. You can see this happening in TextArea/styles.ts and TextArea/index.ts. 👍

I'm not sure there's much value in exporting another component that simply wraps it? The atom would want to be called TextArea, but that's the name of the "main" component too. This isn't a problem with the Accordion atoms because they're all uniquely named 🤔

Will leave up to you. IMO, I am in favour of the implementation you presented - was just a suggestion to alleviate any concerns Adrian had. Will leave you to guys to agree a preference. If the auto-resizing behaviour is what the design team want, I am very much on board with delivering this.

@kurtdoherty
Copy link
Contributor Author

question: @adrian-reapit just checking in to see if your concerns about the approach taken in this PR and #151 are "blocking", or if you're okay for me to proceed? If they're blocking, I'd like to line up a video call for us to chat through our options as soon as possible so we can get this work finished off and merged in 🙏

@kurtdoherty
Copy link
Contributor Author

kurtdoherty commented Aug 8, 2024

From @adrian-reapit via a Teams chat:

Its not blocking but iIs a noticeable change.. I would just give the choice until it had full browser support and didn't need a fallback. We have 12 uses of textarea in our application which will need to be checked so that min/max are set correctly and our site is all mobile usage so the 'fallback' is the component. Everyone updating to v5 will need to do the same on top of all other changes. testers would need to know that the desktop version is not the same as mobile. Happy to discuss if you want

I appreciate the concern around the adoption/migration cost for v4 consumers. Console Cloud has a significant migration ahead of it too. A few thoughts though:

  • The Design System wants text area's to have a minimum of 3 rows but no maximum number of rows, by default. This means those 12 text areas would largely behave the same as they do now, only they'll resize themselves, not allow the user to do that themselves. Conceptually, this seems like a pretty safe change for those 12 text areas, but I acknowledge there may be nuances to those 12 uses that I'm not aware of that make this change more concerning.
  • Off the back of a conversation with Andrei, I need to tweak the component interface by adding a new prop, fieldSizing with two values fixed and content. While content would be the default value for this prop, we could add a third, deprecated option, manual, that would restore the manual resizing behaviour the v4 text area currently has. This would mean v4 consumers that want to maintain this existing behaviour would just need to add fieldSizing="manual". Would this be reasonable?

@kurtdoherty kurtdoherty force-pushed the 136-v5-component-refactor-textarea branch from 844f2d4 to 509ae02 Compare August 11, 2024 23:27
@kurtdoherty kurtdoherty force-pushed the 136-v5-component-refactor-textarea branch from 509ae02 to 2f8e95e Compare August 12, 2024 02:09
@kurtdoherty
Copy link
Contributor Author

kurtdoherty commented Aug 12, 2024

From @adrian-reapit via Teams chat:

That suggestion would be ok. Thanks

Done 👍

I've introduced the new fieldSizing: content | fixed | manual prop as per designs feedback (and yours re manual sizing). Merging this PR in now, so that we can get this work into final review with design.

@kurtdoherty kurtdoherty merged commit a24905a into main Aug 12, 2024
4 checks passed
@kurtdoherty kurtdoherty deleted the 136-v5-component-refactor-textarea branch August 12, 2024 02:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
v5 Issues for v5 release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants