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

NavigationTabs Styling #2508

Open
wants to merge 45 commits into
base: feature/tabs
Choose a base branch
from
Open

NavigationTabs Styling #2508

wants to merge 45 commits into from

Conversation

beaesguerra
Copy link
Member

@beaesguerra beaesguerra commented Mar 20, 2025

Summary:

The following changes address various styling requirements and includes adding some tools for testing in Storybook

Props

  • NavigationTabs supports styles prop and NavigationTabs supports style prop for custom styling

Styles

  • States for current, press, hover, and focus (using a box shadow) are implemented
  • Semantic color and sizing tokens are used (colors may need to be revisited once Thunder Blocks theme is implemented)
  • Used box-shadow for the underline. This doesn't conflict with the box-shadow used for the focus outline of the link because the underline is on the parent <li> element instead of the <a>
  • Long labels within the tab won't wrap. No max-width is set at the time to better support translated strings
  • The navigation tabs will horizontally scroll if the tabs don't fit on the screen
  • Styling is tested with Link, anchor tags, and IconButton
  • Handles RTL

Storybook

  • Added a decorator and toolbar control to set the language direction
  • Added a decorator and toolbar control to set specific zoom presets
  • New constants file for text to use for testing different cases (text-for-testing.ts)
  • New ScenariosLayout component for laying out various scenarios in a list
  • Making AllVariants responsive and supporting a list only mode (useful for testing zoom for all variants)

Issue: WB-1898

Test plan:

  • Review stories and docs for NavigationTabs and NavigationTabItem
  • Testing rtl, zoom, different browsers, responsiveness

…er. Since it's put on the li, it won't conflict with the focus outline box shadow later on
@beaesguerra beaesguerra self-assigned this Mar 20, 2025
Copy link

changeset-bot bot commented Mar 20, 2025

🦋 Changeset detected

Latest commit: 9c4fa3d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@khanacademy/wonder-blocks-tabs Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Contributor

github-actions bot commented Mar 20, 2025

Size Change: +299 B (+0.3%)

Total Size: 100 kB

Filename Size Change
packages/wonder-blocks-tabs/dist/es/index.js 1.24 kB +299 B (+31.67%) 🚨
ℹ️ View Unchanged
Filename Size
packages/wonder-blocks-accordion/dist/es/index.js 3.54 kB
packages/wonder-blocks-banner/dist/es/index.js 1.55 kB
packages/wonder-blocks-birthday-picker/dist/es/index.js 1.88 kB
packages/wonder-blocks-breadcrumbs/dist/es/index.js 886 B
packages/wonder-blocks-button/dist/es/index.js 4.34 kB
packages/wonder-blocks-cell/dist/es/index.js 2.35 kB
packages/wonder-blocks-clickable/dist/es/index.js 3.07 kB
packages/wonder-blocks-core/dist/es/index.js 2.85 kB
packages/wonder-blocks-data/dist/es/index.js 6.25 kB
packages/wonder-blocks-dropdown/dist/es/index.js 19.5 kB
packages/wonder-blocks-form/dist/es/index.js 6.04 kB
packages/wonder-blocks-grid/dist/es/index.js 1.36 kB
packages/wonder-blocks-icon-button/dist/es/index.js 3.15 kB
packages/wonder-blocks-icon/dist/es/index.js 873 B
packages/wonder-blocks-labeled-field/dist/es/index.js 1.22 kB
packages/wonder-blocks-layout/dist/es/index.js 1.82 kB
packages/wonder-blocks-link/dist/es/index.js 2.04 kB
packages/wonder-blocks-modal/dist/es/index.js 5.5 kB
packages/wonder-blocks-pill/dist/es/index.js 1.49 kB
packages/wonder-blocks-popover/dist/es/index.js 4.92 kB
packages/wonder-blocks-progress-spinner/dist/es/index.js 1.52 kB
packages/wonder-blocks-search-field/dist/es/index.js 1.32 kB
packages/wonder-blocks-switch/dist/es/index.js 2.02 kB
packages/wonder-blocks-testing-core/dist/es/index.js 3.91 kB
packages/wonder-blocks-testing/dist/es/index.js 1.07 kB
packages/wonder-blocks-theming/dist/es/index.js 679 B
packages/wonder-blocks-timing/dist/es/index.js 1.79 kB
packages/wonder-blocks-tokens/dist/es/index.js 2.7 kB
packages/wonder-blocks-toolbar/dist/es/index.js 923 B
packages/wonder-blocks-tooltip/dist/es/index.js 7.01 kB
packages/wonder-blocks-typography/dist/es/index.js 1.23 kB

compressed-size-action

Copy link
Contributor

github-actions bot commented Mar 20, 2025

A new build was pushed to Chromatic! 🚀

https://5e1bf4b385e3fb0020b7073c-faxousjuul.chromatic.com/

Chromatic results:

Metric Total
Captured snapshots 404
Tests with visual changes 0
Total stories 594
Inherited (not captured) snapshots [TurboSnap] 0
Tests on the build 404

const withZoom: Decorator = (Story, context) => {
if (context.globals.zoom) {
return (
<div style={{ zoom: context.globals.zoom }}>
Copy link
Member Author

Choose a reason for hiding this comment

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

I was trying to find a way for us to easily configure a story to be a specific zoom level so we can have some Chromatic tests covering those cases too. I came across the CSS zoom property to control magnification

Comment on lines 179 to 220
direction: {
description: "The language direction to use",
toolbar: {
title: "Language Direction",
icon: "globe",
items: [
{
value: "ltr",
icon: "arrowrightalt",
title: "Left to Right",
},
{
value: "rtl",
icon: "arrowleftalt",
title: "Right to Left",
},
],
dynamicTitle: true,
},
},
zoom: {
description: "Preset zoom level",
toolbar: {
title: "Zoom Presets",
icon: "zoom",
items: [
{
// undefined so the there is no zoom value set
value: undefined,
title: "default",
},
{
value: "2",
title: "200%",
},
{
value: "4",
title: "400%",
},
],
},
}
Copy link
Member Author

Choose a reason for hiding this comment

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

There are 2 new controls in our toolbar! We can also configure these global parameters for specific stories

Screenshot 2025-03-20 at 1 33 43 PM Screenshot 2025-03-20 at 1 36 04 PM

</View>
)}
</AllVariants>
<div dir="rtl">
Copy link
Member Author

Choose a reason for hiding this comment

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

Including the rtl block as part of the same all variants story so that it can be covered in the different states (hover, focus, etc)

Copy link
Member

Choose a reason for hiding this comment

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

Nice! I did something similar with Link here:

const themes: Array<string> = ["default", "dark", "rtl"];
type Story = StoryObj<typeof Link>;
/**
* The following stories are used to generate the pseudo states for the Radio
* component. This is only used for visual testing in Chromatic.
*/
const meta = {
title: "Packages / Link / Link - All Variants",
render: (args) => (
<View style={styles.container}>
{themes.map((theme, idx) => (
<View style={[styles.theme, styles[theme]]} key={idx}>
<HeadingLarge style={styles.title}>{theme}</HeadingLarge>
<AllVariants rows={rows} columns={columns}>
{(props) => (
<>
<Link
{...args}
{...props}
light={theme === "dark"}
href="https://www.khanacademy.org"
>
{theme === "rtl"
? "هذا الرابط مكتوب باللغة العربية"
: "This is a Link"}
</Link>
</>
)}
</AllVariants>
</View>
))}
</View>
),

Copy link
Member

Choose a reason for hiding this comment

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

For later.... it would be nice defining a general pattern in all-variants.tsx to include RTL by default, but we can define that later if needed.

Copy link
Member Author

Choose a reason for hiding this comment

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

Haha yes I was referencing your work for the AllVariants with rtl!

For later.... it would be nice defining a general pattern in all-variants.tsx to include RTL by default, but we can define that later if needed.

While working on this, I attempted to implement this because I thought it'd be nice to include the rtl cases by default! I then realized I had to change the labels for rtl so it wasn't using English. Thinking back, this probably could be solved by passing a boolean to the children function for whether or not it is rtl. Here's the commit I tried it in 4d233ce .

On a related note, I was also wondering if we wanted the AllVariants component to cover the hover, focus, press states too in one story instead having one story for each state combination. We could do this by wrapping each table with an id, and then specifying specific selectors when we configure the pseudo states add on:

parameters: {
      pseudo: {
          hover: [
              `#${stateElementIds.hover} *`,
              `#${stateElementIds.hoverAndFocus} *`,
          ],
          focusVisible: [
              `#${stateElementIds.focus} *`,
              `#${stateElementIds.hoverAndFocus} *`,
          ],
          active: [`#${stateElementIds.press} *`],
      },
  },

While it's possible, I'm not sure if it would be helpful to include it all in one sticker sheet. It would save snapshot amounts though since we'd have ~5 snapshots per component due to the different states!

Curious to hear if anyone has thoughts on this! Happy to iterate on this in another PR :)

Copy link
Member

Choose a reason for hiding this comment

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

oh merging pseudo states would be worth experimenting, and then we could see if there's benefit from that change. how does that sound?

Copy link
Member Author

Choose a reason for hiding this comment

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

I experimented with merging the pseudo states and rtl in the same story: #2511

Left some comments in the draft PR too, feel free to provide feedback in that PR!

</div>
),
globals: {
zoom: "400%",
Copy link
Member Author

Choose a reason for hiding this comment

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

Here's an example of how we can configure the new zoom decorator!

Copy link
Member

Choose a reason for hiding this comment

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

That is so useful!!

@beaesguerra beaesguerra force-pushed the nav-tabs-style branch 3 times, most recently from 897067d to a09e8f5 Compare March 20, 2025 21:07
parameters: {
chromatic: {
// Disabling because Chromatic crops the story when zoom is used
disableSnapshot: true,
Copy link
Member Author

Choose a reason for hiding this comment

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

I disabled the Zoom stories in Chromatic because the stories would get cropped in the snapshots 😢 here is an example in Chromatic. I tried some things to address this but couldn't get it working. Let me know if you have other ideas!

  • setting the viewport config for Chromatic for that story
  • adding a delay for the snapshot
  • setting width: 100% on the decorator

image

Copy link
Member

Choose a reason for hiding this comment

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

Weird. Maybe try min-width: 100vw, and/or some borders to see if you can determine which top-level element is being cut off?

Copy link
Member

Choose a reason for hiding this comment

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

have you tried using viewports?

chromatic: {
// NOTE: This is required to prevent Chromatic from cutting off the
// dark background in screenshots (accounts for all the space taken
// by the variants).
viewports: [1700],
},

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the suggestions!

  • Setting min-width: 100vw made it so the NavigationTabs are no longer horizontally scrollable since it allows the container to expand. We want to be able to observe the scrolling behaviour!
  • Related to that, I tried setting width: 100vw and maxWidth: 100vw. The snapshot shows more of the contents, though we lose the horizontal scrolling in the NavigationTabs (it's put on the story instead). Chromatic snapshot
  • Setting viewports didn't work. Chromatic with viewports config
  • Setting defaultViewport didn't work too (the snapshot was less wide too for some reason) Chromatic with defaultViewport

I went back to disabling the Zoom stories 😅 I think at least the stories are functional and show the expected behaviour when you try it out in Storybook! One other idea is we could reach out to the Chromatic team to see if they've seen this issue before. (I think it's a Chromatic issue since the story renders as expected and it's only the snapshot that is cropped!). @jandrade how do you normally contact Chromatic for support?

const styles = StyleSheet.create({
root: {
listStyle: "none",
display: "inline-flex",
[":has(a:hover)" as any]: {
boxShadow: `inset 0 -${sizing.size_050} 0 0 ${semanticColor.action.secondary.progressive.hover.foreground}`,
Copy link
Member Author

Choose a reason for hiding this comment

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

I mostly used the secondary action semantic tokens. We may need to revisit this though once Thunder Blocks theming is in place. (related Figma thread)

Copy link
Member

Choose a reason for hiding this comment

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

question: Do you know if this is the right box-shadow width? I was looking at the design specs, and it looks like the underline width for hover is 2px (size_025).

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch!! Updated the hover underline to be 2px tall. I also made sure the current + hover state remains the same at 4px!

@beaesguerra beaesguerra marked this pull request as ready for review March 20, 2025 21:35
@khan-actions-bot khan-actions-bot requested a review from a team March 20, 2025 21:35
@khan-actions-bot
Copy link
Contributor

Gerald

Required Reviewers
  • @Khan/wonder-blocks for changes to .changeset/twenty-actors-tease.md, .storybook/preview.tsx, __docs__/components/all-variants.tsx, __docs__/components/scenarios-layout.tsx, __docs__/components/text-for-testing.ts, __docs__/wonder-blocks-tabs/navigation-tab-item-variants.stories.tsx, __docs__/wonder-blocks-tabs/navigation-tab-item.argtypes.tsx, __docs__/wonder-blocks-tabs/navigation-tab-item.stories.tsx, __docs__/wonder-blocks-tabs/navigation-tabs-variants.stories.tsx, __docs__/wonder-blocks-tabs/navigation-tabs.argtypes.tsx, __docs__/wonder-blocks-tabs/navigation-tabs.stories.tsx, packages/wonder-blocks-tabs/src/components/navigation-tab-item.tsx, packages/wonder-blocks-tabs/src/components/navigation-tabs.tsx

Don't want to be involved in this pull request? Comment #removeme and we won't notify you of further changes.

Copy link
Contributor

github-actions bot commented Mar 20, 2025

npm Snapshot: Published

🎉 Good news!! We've packaged up the latest commit from this PR (cac8b31) and published all packages with changesets to npm.

You can install the packages in webapp by running:

./services/static/dev/tools/deploy_wonder_blocks.js --tag="PR2508"

Packages can also be installed manually by running:

pnpm add @khanacademy/wonder-blocks-<package-name>@PR2508

Copy link
Member

@marcysutton marcysutton left a comment

Choose a reason for hiding this comment

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

Great work @beaesguerra! I left you a few really tiny suggestions. Excited to see this component in the wild!

};

/**
* The following story shows how the component habdles specific scenarios.
Copy link
Member

Choose a reason for hiding this comment

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

nit: tiny typo!

Suggested change
* The following story shows how the component habdles specific scenarios.
* The following story shows how the component handles specific scenarios.

icon: "zoom",
items: [
{
// undefined so the there is no zoom value set
Copy link
Member

Choose a reason for hiding this comment

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

nit: extra word?

Suggested change
// undefined so the there is no zoom value set
// undefined so there is no zoom value set

...row.props,
...col.props,
},
`${row.name} ${col.name}`,
Copy link
Member

Choose a reason for hiding this comment

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

Oh interesting, I just did something super similar in #2509!

Copy link
Member Author

Choose a reason for hiding this comment

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

Nice!! I was also addressing a11y warnings in the new stories! I see in #2509 we add aria-label to the props. I ended up adding a new argument for the name so it could be used for a unique aria-label. I was thinking not all components may support an aria-label at the root level so providing it as an arg keeps it flexible! What do you think?

</div>
),
globals: {
zoom: "400%",
Copy link
Member

Choose a reason for hiding this comment

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

That is so useful!!

parameters: {
chromatic: {
// Disabling because Chromatic crops the story when zoom is used
disableSnapshot: true,
Copy link
Member

Choose a reason for hiding this comment

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

Weird. Maybe try min-width: 100vw, and/or some borders to see if you can determine which top-level element is being cut off?

a11y: {
config: {
rules: [
// Disabling warning: "Element's background color could not
Copy link
Member

Choose a reason for hiding this comment

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

I'm curious about this one, as I wrote part of the overlapping logic in the color-contrast rule! It looks like the Axe extension doesn't surface incompletes anymore, and I don't see any Needs Review items in Accessibility Insights either. Maybe Storybook is using an older axe-core API version?

Copy link
Member Author

Choose a reason for hiding this comment

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

It was showing as an incomplete in the Storybook add on! Any tips on how to address this?

image

I also see it in the labels for the story! Might be related to setting the CSS zoom property?
image

alt="Wonder Blocks logo"
/>
<SingleSelect
placeholder="Placeholder"
Copy link
Member

Choose a reason for hiding this comment

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

This one needs a label with the new API!

Suggested change
placeholder="Placeholder"
aria-label="Dropdown"
placeholder="Placeholder"

Copy link
Member

@jandrade jandrade left a comment

Choose a reason for hiding this comment

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

Thanks for working on this and adding all these a11y features, it is going to be REALLY useful in general. I've left some comments for you to consider before giving a final approval.

Copy link
Member

Choose a reason for hiding this comment

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

praise: This is super useful!

</View>
)}
</AllVariants>
<div dir="rtl">
Copy link
Member

Choose a reason for hiding this comment

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

Nice! I did something similar with Link here:

const themes: Array<string> = ["default", "dark", "rtl"];
type Story = StoryObj<typeof Link>;
/**
* The following stories are used to generate the pseudo states for the Radio
* component. This is only used for visual testing in Chromatic.
*/
const meta = {
title: "Packages / Link / Link - All Variants",
render: (args) => (
<View style={styles.container}>
{themes.map((theme, idx) => (
<View style={[styles.theme, styles[theme]]} key={idx}>
<HeadingLarge style={styles.title}>{theme}</HeadingLarge>
<AllVariants rows={rows} columns={columns}>
{(props) => (
<>
<Link
{...args}
{...props}
light={theme === "dark"}
href="https://www.khanacademy.org"
>
{theme === "rtl"
? "هذا الرابط مكتوب باللغة العربية"
: "This is a Link"}
</Link>
</>
)}
</AllVariants>
</View>
))}
</View>
),

</View>
)}
</AllVariants>
<div dir="rtl">
Copy link
Member

Choose a reason for hiding this comment

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

For later.... it would be nice defining a general pattern in all-variants.tsx to include RTL by default, but we can define that later if needed.

Comment on lines 116 to 122
<div
style={{
padding: sizing.size_100,
marginBlock: sizing.size_100,
border: `${border.width.hairline}px dashed ${semanticColor.border.primary}`,
}}
>
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: It would be great switching to View (or StyledDiv if view doesn't fit) to prevent inlining the CSS in the html.

Suggested change
<div
style={{
padding: sizing.size_100,
marginBlock: sizing.size_100,
border: `${border.width.hairline}px dashed ${semanticColor.border.primary}`,
}}
>
<View
style={{
padding: sizing.size_100,
marginBlock: sizing.size_100,
border: `${border.width.hairline}px dashed ${semanticColor.border.primary}`,
}}
>

Expanding on this, the styles could be moved to the styles object as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the reminder! Updated :)

</View>
)}
</AllVariants>
<div>
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: You could also use a fragment to avoid adding extra divs.

Suggested change
<div>
<>

</View>
)}
</AllVariants>
<div>
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
<div>
<>

@@ -141,6 +227,58 @@ export const Active: Story = {
parameters: {pseudo: {hover: true, active: true}},
};

export const Zoom: Story = {
render: (args) => (
<div>
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
<div>
<>

color: semanticColor.action.secondary.progressive.default
.foreground,
outline: "none",
boxShadow: `0 0 0 ${sizing.size_025} ${semanticColor.focus.inner}, 0 0 0 ${sizing.size_050} ${semanticColor.focus.outer}`,
Copy link
Member

Choose a reason for hiding this comment

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

praise: Yay!! it is great finally seeing both semantic focus tokens in place 🎉

const styles = StyleSheet.create({
root: {
listStyle: "none",
display: "inline-flex",
[":has(a:hover)" as any]: {
boxShadow: `inset 0 -${sizing.size_050} 0 0 ${semanticColor.action.secondary.progressive.hover.foreground}`,
Copy link
Member

Choose a reason for hiding this comment

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

question: Do you know if this is the right box-shadow width? I was looking at the design specs, and it looks like the underline width for hover is 2px (size_025).

Comment on lines +39 to +40
root?: StyleType;
list?: StyleType;
Copy link
Member

Choose a reason for hiding this comment

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

thought: What do you think about extending the description a bit to specify what root and list are specifically for?

Copy link
Member Author

Choose a reason for hiding this comment

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

Added more docs!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants