Skip to content

feat(components): Feedback on Layout 2.0 #2485

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

Merged
merged 19 commits into from
Apr 15, 2025

Conversation

scotttjob
Copy link
Contributor

@scotttjob scotttjob commented Apr 15, 2025

Motivations

  1. Got some feedback on the Layout components.
  2. Wanted the review of the feedback to be easy (this is a big PR), so I broke the feedback out into its own PR so it can be reviewed independently.
  3. If we approve this PR, it will be an implied approval on the original Layout PR as well (we can rubber stamp that through, just to make things less complicated).

Changes

  1. Updated 'space' token for 'gap' everywhere in the new layout components. There was a few references, and they've all been updated.
  2. Removed useMediaQuery usages for 'collapseBelow'. Instead relying on common Atlantis breakpoints. Also included a 'collapsed' prop so the consumer can have completely control beyond the standard breakpoints.
  3. Added the ability to set new layout components to specified list of semantic HTML elements (additional elements can be added in the future if needed).
  4. Added the ability for data and aria pass through, open to alternative patterns for this approach.
  5. Added role/id pass through for all components, along with UNSAFE styles and classnames.
  6. Updated naming of some functions away from 'use' based react hooks to 'get' based standard functions.
  7. Moved all Inline styles configuration out of disconnected (and prematurely optimized) useMemo instances.
  8. Updated some descriptions to be clearer

Changes can be
tested via Pre-release


In Atlantis we use Github's built in pull request reviews.

Random photo of Atlantis

Copy link

cloudflare-workers-and-pages bot commented Apr 15, 2025

Deploying atlantis with  Cloudflare Pages  Cloudflare Pages

Latest commit: 74ab561
Status: ✅  Deploy successful!
Preview URL: https://b1317577.atlantis.pages.dev
Branch Preview URL: https://scott-t-layout-feedback.atlantis.pages.dev

View logs

Copy link
Contributor

@ericchernuka ericchernuka left a comment

Choose a reason for hiding this comment

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

Overall liking the direction and flexibility. Few questions.

* More information in the [Customizing components Guide](https://atlantis.getjobber.com/guides/customizing-components).
*/
readonly UNSAFE_className?: {
container?: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

I had a question on this? I realize its consistent, but requires an object to be created for elements with only 1 component like this? Same comment about 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.

Yeah I went for consistency over what felt like the shortest path. I'll double check with the team to see if the string-only option is acceptable within the pattern, because I would prefer it myself for this.

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 it could become a clear rule that if it has more than one override, then it's an object; otherwise, it's a string.

Copy link
Contributor

@taylorvnoj taylorvnoj Apr 15, 2025

Choose a reason for hiding this comment

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

@jdeichert, @ZakaryH & I have discussed this before when adding UNSAFE_ to atom components that are also just a container. Jacob's suggestion was that we used a keyed map to make sure we're handling future scenarios/future proofing (even if it seems unlikely any other sub element will be added to a component).
I did initially have the same thoughts, and we could have just a string for className and an object for _styles if people feel strongly. I tend to lean into consistency at this point.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's fair. I think it probably speaks to components needing more composition so that the overrides could be targetted, but another way to approach that with the current structure would be to actually give data-slot="<componentName>" which could then be targetted by the CSS through data attribute selectors rather than needing to be the ones responsible for plumbing classnames through.

Forgive the tailwind nuances here that they're hiding behind Components, but he presents an interesting approach to controlling styles from above using data attributes. https://www.youtube.com/watch?v=MrzrSFbxW7M

Copy link
Contributor

Choose a reason for hiding this comment

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

I was concerned about future-proofing because there are cases where we have to go in and wrap an extra div sometimes.

However, I'm realizing that typescript could allow us to support both scenarios. We could start off with a string type, and IF we ever need to expand this in the future, we could also accept an object with keys.

I can't remember if I had any other pro-object arguments, but I'm less concerned now.

Copy link
Contributor

Choose a reason for hiding this comment

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

}) {
return <div className={styles.centerContent}>{children}</div>;
return (
<div
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this also have a Tag?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure should! Nice catch.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed!

Comment on lines 1 to 5
export function dataPropsMapped(data?: Record<string, string>) {
return Object.entries(data || {}).map(([key, value]) => ({
[key]: value,
}));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Is data supposed to be used like

data={{ 'data-foo': true }}

or

data={{ foo: true }}

Copy link
Contributor

Choose a reason for hiding this comment

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

Guess the other option is to just extend DataHTMLAttributes from React?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I updated this while you were reviewing (my spidey sense was tingling)! it's now a proper data- attribute and it lets you know how to use it.

Copy link
Contributor

Choose a reason for hiding this comment

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

@scotttjob instead of Record<string, string> you can possibly make a typescript key type that checks the prefix and this will guarantee it only allows data- keys

I can hack up something today as an example 👍

Copy link
Contributor

@jdeichert jdeichert Apr 15, 2025

Choose a reason for hiding this comment

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

interface DataAttributes {
  [key: `data-${string}`]: string;
}

export function dataPropsMapped(data?: DataAttributes) {
  return Object.entries(data || {}).map(([key, value]) => ({
    [key]: value,
  }));
}

If you use this DataAttributes type as the data prop on components, it will flow through here properly, and the prop will block incorrect usages of it!

Copy link
Contributor

@jdeichert jdeichert Apr 15, 2025

Choose a reason for hiding this comment

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

I noticed you already used data?: { [key: data-${string}]: string }; in CommonAtlantisProps. Ignore me 😅

Copy link
Contributor

Choose a reason for hiding this comment

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

@ericchernuka something I learned about DataHTMLAttributes is that it pulls in a lot more attributes than you would expect since it also extends HTMLAttributes (which extends AriaAttributes and DOMAttributes).

Copy link
Contributor

@jdeichert jdeichert Apr 15, 2025

Choose a reason for hiding this comment

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

I believe that's because it's for the data element: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/data (just like how InputHTMLAttributes is for input)

It's not actually data attributes as far as I can tell

Copy link
Contributor

Choose a reason for hiding this comment

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

Yup. I dug into the types and was like why am I getting aria attributes!?

Comment on lines 7 to 11
export function ariaPropsMapped(aria?: React.AriaAttributes) {
return Object.entries(aria || {}).map(([key, value]) => ({
[key]: value,
}));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment about aria attributes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Wonder if we can just extend AriaAttributes from React?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Donezo! This was one was in flight too while you were reviewing. You can now pass fully qualified aria- and it autocompletes for you.

Comment on lines +1 to +7
export const AtlantisBreakpoints = {
xs: 0,
sm: 490,
md: 768,
lg: 1080,
xl: 1440,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

I noticed that we seem to redefine these across many components? Do we need a central version that they all extend 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.

Yeah it's going to be this one. We seemed to have a pattern in the past where we didn't re-use anything across components unless they got promoted to hooks, but that's too much effort for some of these component-only usages.

@taylorvnoj
Copy link
Contributor

Looks really good to me. We have been adding UNSAFE_ documentation on Implement tabs but that and other Implement tab type info can definitely be in a follow up. I might even be able to do some of that work during my support hero rotation that starts today.
Not sure if you have any other changes for this PR - ping me for when ready/when circleci is green 👍🏻

/** Standard HTML id attribute. */
id?: string;
}
export type CommonAllowedElements =
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's also add p which would be useful in some scenerios

* More information in the [Customizing components Guide](https://atlantis.getjobber.com/guides/customizing-components).
*/
readonly UNSAFE_className?: {
container?: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

That's fair. I think it probably speaks to components needing more composition so that the overrides could be targetted, but another way to approach that with the current structure would be to actually give data-slot="<componentName>" which could then be targetted by the CSS through data attribute selectors rather than needing to be the ones responsible for plumbing classnames through.

Forgive the tailwind nuances here that they're hiding behind Components, but he presents an interesting approach to controlling styles from above using data attributes. https://www.youtube.com/watch?v=MrzrSFbxW7M

* More information in the [Customizing components Guide](https://atlantis.getjobber.com/guides/customizing-components).
*/
readonly UNSAFE_className?: {
container?: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

readonly children: React.ReactNode;
/** The width of the sidekick. */
readonly sideWidth?: string;
/** The minimum width of the content. */
readonly contentMinWidth?: string;
/** The amount of space between the sidekick and the content. Semantic tokens are available. */
readonly space?: string | Spaces;
readonly gap?: string | Spaces;
Copy link
Contributor

Choose a reason for hiding this comment

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

I see that in other places you used a slightly different type:

readonly gap?: Spaces | (string & NonNullable<unknown>);

Is it intentionally different here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not intentionally no! I've replaced these various Spaces | options with a new 'GapSpacing' type that should be used everywhere this loose type appeared before.

readonly children: React.ReactNode;
/** The amount of space between the children. Semantic tokens are available. */
readonly space?: string | Spaces;
/** The number of children to split the stack after (1-15). Requires parent to have height greater than the sum of the children. */
readonly gap?: string | Spaces;
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as above regarding the type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Replaced with the above!

readonly children: React.ReactNode;
/** The minimum size of the tiles. */
readonly minSize: string;
/** The amount of space between the tiles. Semantic tokens are available. */
readonly space: string | Spaces;
readonly gap: string | Spaces;
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above. Also is gap required for Tiles?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gap is not required! Good catch. I've updated this one as well with the new type

Copy link
Contributor

@jdeichert jdeichert left a comment

Choose a reason for hiding this comment

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

Looks good! I'll approve the main PR once this one merges down.

@taylorvnoj taylorvnoj self-requested a review April 15, 2025 18:11
@scotttjob scotttjob merged commit 0daa163 into scott_t/layouts Apr 15, 2025
10 checks passed
@scotttjob scotttjob deleted the scott_t/layout-feedback branch April 15, 2025 18:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

6 participants