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

feat(components): added a component called Tabs #15437

Merged
merged 23 commits into from
Jul 2, 2024

Conversation

syao1226
Copy link
Collaborator

@syao1226 syao1226 commented Jun 17, 2024

Overview

RSQ-86: Create a new Tabs component to unify the TabbedButton/RoundTab components in storybook.

https://www.figma.com/design/4gRzu3rkNDZnDPkP36YxfF/Chip%2C-Tag-and-Tab?node-id=123-57775&t=zucfumEcRony14Xm-0

Test Plan

Changelog

Review requests

Risk assessment

handleClick(index)
button.onClick()
}}
className={index === activeTab ? 'active' : ''}
Copy link
Member

Choose a reason for hiding this comment

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

what is this className doing? since we use styled-components instead of raw css we don't need to apply css classes do we?

Comment on lines 71 to 72
handleClick(index)
button.onClick()
Copy link
Member

Choose a reason for hiding this comment

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

i think having the button both update its internal state (activeTab ) as well as call the provided onClick handler is a little confusing because you have two sources of truth for whether or not a button is active (this component's internal react state, as well as the provided isActive prop). i prefer having the parent component tell us whether or not a button is active, so i think we should get rid of the internal activeTab react state variable

Comment on lines 54 to 58
const [activeTab, setActiveTab] = React.useState<number | null>(null);

const handleClick = (index: number) : void => {
setActiveTab(index)
}
Copy link
Member

Choose a reason for hiding this comment

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

see comment below, i dont think we need this!

Comment on lines 76 to 77

css={index === activeTab || button.isActive === true ? currentTabStyle : button.disabled === true ? disabledTabStyle : defaultTabStyle}
Copy link
Member

@shlokamin shlokamin Jun 17, 2024

Choose a reason for hiding this comment

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

instead of conditionally choosing a CSS block for disabled, you can tell an HTML element that it is disabled by providing the disabled attribute: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled

this will simplify the ternary 😄

@syao1226 syao1226 force-pushed the components-unify-tab-button branch from 3aeb35b to a6e12e9 Compare June 18, 2024 17:38
Comment on lines 1 to 4
import * as React from 'react'
import { Tabs as TabComponent } from './Tabs'
import type { Meta, StoryObj } from '@storybook/react'
import { useArgs } from '@storybook/preview-api';
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: we are very particular about import order, the pattern goes:

  • package imports
  • top level directory imports (like opentrons/shared-data or opentrons/components)
  • imports from the farthest path to the shortest so ../../ should be above ./ for example
  • types (type imports should also follow the above pattern)
Suggested change
import * as React from 'react'
import { Tabs as TabComponent } from './Tabs'
import type { Meta, StoryObj } from '@storybook/react'
import { useArgs } from '@storybook/preview-api';
import * as React from 'react'
import { useArgs } from '@storybook/preview-api';
import { Tabs as TabComponent } from './Tabs'
import type { Meta, StoryObj } from '@storybook/react'

Copy link
Member

Choose a reason for hiding this comment

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

thanks jethary, i forgot to explain this to shiyao

import { TYPOGRAPHY, SPACING } from '../ui-style-constants'
import { COLORS, BORDERS } from '../helix-design-system'
import { POSITION_RELATIVE, DIRECTION_COLUMN, DIRECTION_ROW } from '../styles'
import { Flex } from '../primitives'
Copy link
Collaborator

Choose a reason for hiding this comment

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

import order is good here! 👍

Comment on lines 43 to 46
text: string
isActive?: boolean
disabled?: boolean
onClick: () => void
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: optional props should go last

Suggested change
text: string
isActive?: boolean
disabled?: boolean
onClick: () => void
text: string
onClick: () => void
isActive?: boolean
disabled?: boolean

onClick={() => {
button.onClick()
}}
css={button.isActive === true ? currentTabStyle : defaultTabStyle}
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: i think this works as a shorthand

Suggested change
css={button.isActive === true ? currentTabStyle : defaultTabStyle}
css={button.isActive ? currentTabStyle : defaultTabStyle}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've tried deleting the "=== true" statement and it gives me this problem: Unexpected nullable boolean value in conditional. Please handle the nullish case explicitly.eslint@typescript-eslint/strict-boolean-expressions

Copy link
Collaborator

Choose a reason for hiding this comment

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

i see, i guess its because isActive can be undefined. you can keep it as is!


export function Tabs(props: TabsProps): JSX.Element {

const { buttons } = props
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 using interface you defined?

const = {text, onClick, isActive = false, disabled = false } =  props

Copy link
Collaborator Author

@syao1226 syao1226 Jun 18, 2024

Choose a reason for hiding this comment

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

I have the text, onClick, isActive, and disabled wrapped inside an array in the interface because we need to show multiple buttons, so i am not able to call those props directly i think

Copy link
Contributor

Choose a reason for hiding this comment

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

I recommend you use tabs instead of buttons since you button tag.

Copy link
Contributor

Choose a reason for hiding this comment

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

I checked a couple of ui components.
they use the following structure and I think RoundTab is also the same.
I guess passing a custom array to ui component is unusual.

<Tabs>
  <Tab value="1">tab1<Tab>
  <Tab value="2">tab2<Tab>
</Tabs>

Is there any specific reason that you would want to pass an array to this component?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

that's a really good question! based on what Shlok have told me we want the TabsProps take in a list of tabs, where each tab has text, isActive, isDisabled, and onClick. that way we keep the interface terse and generic

Copy link
Contributor

@koji koji Jun 18, 2024

Choose a reason for hiding this comment

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

Copy link
Collaborator

Choose a reason for hiding this comment

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

i think something we can do to make it cleaner is create a new type that includes

        text: string
        isActive?: boolean
        disabled?: boolean
        onClick: () => void

So for example:

interface TabProps {
        text: string
        isActive?: boolean
        disabled?: boolean
        onClick: () => void
}

And then

export interface TabsProps {
      tabs: TabProps[]
}

Because this is the Tabs component that has Tab[] inside of it, i think instead of calling it buttons, it could be tabs?

import { POSITION_RELATIVE, DIRECTION_COLUMN, DIRECTION_ROW } from '../styles'
import { Flex } from '../primitives'

const defaultTabStyle = css`
Copy link
Contributor

Choose a reason for hiding this comment

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

basically we use SCREAMING_SNAKE_CASE for a css var.

Suggested change
const defaultTabStyle = css`
const DEFAULT_TAB_STYLE = css`

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

updated

Copy link
Member

Choose a reason for hiding this comment

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

lol i had no idea it was called SCREAMING_SNAKE_CASE

Copy link
Contributor

@koji koji Jun 18, 2024

Choose a reason for hiding this comment

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

unify the TabbedButton/RoundTab

This means that this new component needs to support ODD style like what Jethary does for Tag.

https://github.com/Opentrons/opentrons/pull/15441/files#diff-69b769c5ad20c1d688db697b1ea496fecad8e6dc0f020bba62e048b1c4d541d0

We use media-query to switch styles. The following for ODD
@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs}

Also, could you write test for the component?

https://opentrons.atlassian.net/wiki/spaces/PAR/pages/4086104119/UI+component+Unification+Implementation+Guide

Copy link
Member

Choose a reason for hiding this comment

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

@koji when you have some time would you mind pairing with Shiyao to show her how to write a test using react testing library?

Copy link
Member

@shlokamin shlokamin 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 so far shiyao! it looks like what's remaining is:

  1. add a test for the component (@koji or @ncdiehl11 can show you how)
  2. add ODD specific style overrides for the style differences in ODD
  3. replace all instances of the old chip and tag with your new component (we can do this after Mel gives us the thumbs up that the component is designed to spec)

@koji
Copy link
Contributor

koji commented Jun 19, 2024

@syao1226

  1. Create Tab component and bring TabbedButton(for ODD) and RoundTab(for desktop) into Tab
    TabbedButton's style should be under @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs}
    I recommend you use Btn component instead of button tag since we use that component.

we use the following structure for a component.
For Tab, the following is the preferred structure.

molecules
└── Tab
    ├── Tab.stories.tsx
    ├── __tests__
    │   └── Tab.test.tsx
    └── index.tsx

[samples]
https://github.com/Opentrons/opentrons/pull/15441/files#diff-69b769c5ad20c1d688db697b1ea496fecad8e6dc0f020bba62e048b1c4d541d0

https://github.com/Opentrons/opentrons/blob/edge/components/src/atoms/Chip/index.tsx

  1. Create Tabs component that has the layout for each app(Desktop app and ODD app)
    Tabs.stories.tsx should have Tabs
molecules
└── Tabs
    ├── Tabs.stories.tsx
    ├── __tests__
    │   └── Tabs.test.tsx
    └── index.tsx
  1. Check Tab and Tabs in Storybook
    They should work on Desktop app and ODD app.
    These components shows different style by switching the screen size like below.
Recording.2024-06-18.211405.mp4
  1. Write a test for each component
    Nick or I can show you how to write a test for a component.

  2. Remove RoundTab stories and TabbledButton stories

If you have any question on this, feel free to reach out to the team/me.

@syao1226 syao1226 marked this pull request as ready for review June 21, 2024 18:24
@syao1226 syao1226 requested a review from a team as a code owner June 21, 2024 18:24
tabs: [
{
text: 'Setup',
isActive: false,
Copy link
Contributor

Choose a reason for hiding this comment

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

when the app displays tabs, setup is selected so settings isActive: true would be designer-friendly.

Suggested change
isActive: false,
isActive: true,


const DEFAULT_TAB_STYLE = css`
${TYPOGRAPHY.pSemiBold}
color: ${COLORS.black90};
Copy link
Contributor

@koji koji Jun 21, 2024

Choose a reason for hiding this comment

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

the default text color is black90 so you don't need this.

L28 in app/src/atoms/GlobalStyle/index.ts

Suggested change
color: ${COLORS.black90};

import { SPACING } from '../../../ui-style-constants'
import { POSITION_RELATIVE } from '../../../styles'
import { renderWithProviders } from '../../../testing/utils'
import { Tabs } from '..'
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
import { Tabs } from '..'
import { Tabs } from '../index'

Comment on lines 82 to 84
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing16}
padding={SPACING.spacing16}
Copy link
Contributor

Choose a reason for hiding this comment

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

flexDirection and gridGap aren't needed since there is one element.

Could you add the Figma link you used to implement this to the pr's overview?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy link
Contributor

@koji koji Jun 21, 2024

Choose a reason for hiding this comment

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

${TYPOGRAPHY.pSemiBold}
color: ${COLORS.black90};
background-color: ${COLORS.purple30};
border: 0px ${BORDERS.styleSolid} ${COLORS.purple30};
Copy link
Contributor

Choose a reason for hiding this comment

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

need this line?
border uses 0px which isn't visible.

@koji
Copy link
Contributor

koji commented Jun 21, 2024

@shlokamin

#15437 (comment)
Could you explain why we need to change the existing implementation to Shiyao's implementation?

@shlokamin
Copy link
Member

#15437 (comment) Could you explain why we need to change the existing implementation to Shiyao's implementation?

@koji im not sure what you mean by "existing implementation". the reason i suggested that this component take in a list of tabs is because of the description of the component in Figma:

"Tabbed interfaces are a way of navigating between multiple panels".

keyword "multiple"!

we will never expose a single Tab interface, it is meant to always be a sequence of tabs (hence the component name)

@shlokamin
Copy link
Member

ultimately i don't really think it matters too much though. having a single tab interface is fine too, but i personally don't see anything wrong with the interface this component currently accepts

@koji
Copy link
Contributor

koji commented Jun 27, 2024

Reference in ne

ultimately i don't really think it matters too much though. having a single tab interface is fine too, but i personally don't see anything wrong with the interface this component currently accepts

My concern is that if there is a request to change the background color of a particular tab or to display an icon, multiples would require additional changes.
I guess that would be one reason that major UI components use a single tab component and tags component to manage tab components.

@shlokamin
Copy link
Member

My concern is that if there is a request to change the background color of a particular tab or to display an icon, multiples would require additional changes.

oh if there is ever a request to do that we would say we can't. that would be a big signal that this abstraction is not correct and that this component should not exist. cc @mmencarelli

parameters: VIEWPORT.touchScreenViewport,
argTypes: {
tabs: {
control: {
Copy link
Contributor

Choose a reason for hiding this comment

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

@syao1226 Have you researched a way to control an element in array?

This isn't must-have but nice-to-have since designers would like to use a select button for disabled instead of changing the boolean directly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I haven't and I'll look into it now

Comment on lines 60 to 64
expect(tab2).not.toHaveStyle(`background-color: ${COLORS.purple50}`)
expect(tab2).not.toHaveStyle(`color: ${COLORS.white}`)

expect(tab3).not.toHaveStyle(`background-color: ${COLORS.purple50}`)
expect(tab3).not.toHaveStyle(`color: ${COLORS.white}`)
Copy link
Member

Choose a reason for hiding this comment

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

no need to check these since you're already asserting these same properties on lines 57-58

Copy link
Member

@shlokamin shlokamin left a comment

Choose a reason for hiding this comment

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

code lgtm

@syao1226 syao1226 merged commit 117afb2 into edge Jul 2, 2024
48 checks passed
@syao1226 syao1226 deleted the components-unify-tab-button branch July 2, 2024 21:06
aaron-kulkarni pushed a commit that referenced this pull request Jul 3, 2024
<!--
Thanks for taking the time to open a pull request! Please make sure
you've read the "Opening Pull Requests" section of our Contributing
Guide:


https://github.com/Opentrons/opentrons/blob/edge/CONTRIBUTING.md#opening-pull-requests

To ensure your code is reviewed quickly and thoroughly, please fill out
the sections below to the best of your ability!
-->

# Overview

<!--
Use this section to describe your pull-request at a high level. If the
PR addresses any open issues, please tag the issues here.
-->

RSQ-86: Create a new Tabs component to unify the TabbedButton/RoundTab
components in storybook.


https://www.figma.com/design/4gRzu3rkNDZnDPkP36YxfF/Chip%2C-Tag-and-Tab?node-id=123-57775&t=zucfumEcRony14Xm-0

# Test Plan

<!--
Use this section to describe the steps that you took to test your Pull
Request.
If you did not perform any testing provide justification why.

OT-3 Developers: You should default to testing on actual physical
hardware.
Once again, if you did not perform testing against hardware, justify
why.

Note: It can be helpful to write a test plan before doing development

Example Test Plan (HTTP API Change)

- Verified that new optional argument `dance-party` causes the robot to
flash its lights, move the pipettes,
then home.
- Verified that when you omit the `dance-party` option the robot homes
normally
- Added protocol that uses `dance-party` argument to G-Code Testing
Suite
- Ran protocol that did not use `dance-party` argument and everything
was successful
- Added unit tests to validate that changes to pydantic model are
correct

-->

# Changelog

<!--
List out the changes to the code in this PR. Please try your best to
categorize your changes and describe what has changed and why.

Example changelog:
- Fixed app crash when trying to calibrate an illegal pipette
- Added state to API to track pipette usage
- Updated API docs to mention only two pipettes are supported

IMPORTANT: MAKE SURE ANY BREAKING CHANGES ARE PROPERLY COMMUNICATED
-->

# Review requests

<!--
Describe any requests for your reviewers here.
-->

# Risk assessment

<!--
Carefully go over your pull request and look at the other parts of the
codebase it may affect. Look for the possibility, even if you think it's
small, that your change may affect some other part of the system - for
instance, changing return tip behavior in protocol may also change the
behavior of labware calibration.

Identify the other parts of the system your codebase may affect, so that
in addition to your own review and testing, other people who may not
have the system internalized as much as you can focus their attention
and testing there.
-->

---------

Co-authored-by: shiyaochen <shiyaochen@admins-MacBook-Pro.local>
Co-authored-by: shiyaochen <shiyaochen@admins-mbp.mynetworksettings.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants