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

Add Breadcrumb component #321

Merged
merged 29 commits into from
Jul 17, 2016

Conversation

layershifter
Copy link
Member

@layershifter layershifter commented Jul 2, 2016

Under Construction

Fixes #184

@codecov-io
Copy link

codecov-io commented Jul 2, 2016

Current coverage is 91.51%

Merging #321 into master will increase coverage by 0.45%

@@             master       #321   diff @@
==========================================
  Files            59         62     +3   
  Lines           738        778    +40   
  Methods           0          0          
  Messages          0          0          
  Branches          0          0          
==========================================
+ Hits            672        712    +40   
  Misses           66         66          
  Partials          0          0          

Powered by Codecov. Last updated by b6d9e94...9398fa5

@levithomason
Copy link
Member

Thanks for the PR, it's looking great so far! It looks like you've probably already read the contributing guidelines. Looking forward to the API spec for this component.

The CONTRIBUTING.md guide details how to go about designing a component API. The Spec out the API section is of primary importance. You can also reference other PRs for example specs, like #281.

Cheers!

@layershifter
Copy link
Member Author

Breadcrumb API Proposal

Breadcrumb

Regarding Semantic UI docs, Breadcrumb can have only variation, so I think there will no be
exceptions about following syntax.

<Breadcrumb>...</Breadcrumb>
<Breadcrumb size='massive'>...</Breadcrumb>

Each breadcrumb contains sections and dividers.

Section

There is a trouble with section, it can be text or link, so I propose to add isLink property (I don't found anything like this in other components).

<Breadcrumb.Section>Home</Breadcrumb.Section>
<Breadcrumb.Section active>Home</Breadcrumb.Section>
<Breadcrumb.Section isLink>Home</Breadcrumb.Section>
<Breadcrumb.Section active isLink>Home</Breadcrumb.Section>

Divider

Breadcrumb component also has divider, but it's not ui divider, so it might be subcomponent.

<span class="divider">/</span>
<i class="right chevron icon divider"></i>

I think of such an implementation:

<Breadcrumb.Divider>/</Breadcrumb.Divider>
<Breadcrumb.Divider icon='right chevron'>/</Breadcrumb.Divider>

All together

<Breadcrumb>
  <Breadcrumb.Section isLink>Home</Breadcrumb.Section>
  <Breadcrumb.Divider icon='right chevron'>/</Breadcrumb.Divider>
  <Breadcrumb.Section active>Catalog</Breadcrumb.Section>
</Breadcrumb>

Feedback welcome 😄

@levithomason
Copy link
Member

levithomason commented Jul 4, 2016

I think you're right about the sub components and props here. Here are my thoughts.

Section

Since props without a value default to true, let's not prefix isLink with is*. We'll use link instead. The Label component has a link prop which changes the tag to an a tag. It also changes to an a tag if you provide an onClick prop, this way you get the pointer cursor if you have a click handler.

I think we can do a similar thing here with the Breadcrumb. I see 3 ways of formatting the Section as a link:

  1. add a link prop
  2. add an onClick prop
  3. add an href prop

If any of these three are present, I think we should use an a tag:

<Breadcrumb.Section link>Home</Breadcrumb.Section>
<Breadcrumb.Section onClick={this.handleClick}>Home</Breadcrumb.Section>
<Breadcrumb.Section href='google.com'>Home</Breadcrumb.Section>
<a class="section">Home</a>
<a class="section">Home</a>
<a class="section" href="google.com">Home</a>

Divider

The default divider is /. When an icon is used, there is no / child. I believe we should default the child to / when there is no icon prop given.

<Breadcrumb.Divider />
// and
<Breadcrumb.Divider icon='right chevron' />
<div class="divider"> / </div>
<i class="right chevron icon divider"></i>

To ensure the user doesn't put both an icon prop and children, we have a customPropTypes in propUtils called mutuallyExclusive to prevent the user from defining two conflicting props.

Icon

We may also extend Icon to allow a divider prop: The more I thought about this the less it made sense. This prop wouldn't be used anywhere else that I can see. It isn't an attribute of all icons so I no longer think we should add this.

<Icon name='right chevron' divider />
<i class="right chevron icon divider"></i>

See #279 for that component update. I'll /cc @jamiehill for this addition.

Using Props

We've also been allowing components to define basic markup through props. We would make children mutuallyExclusive with each of these props since these props will generate the children for you.

I can see allowing something like this:

const sections = []

<Breadcrumb sections={sections} />

Regular

const sections = [
  { text: 'Home' },
  { text: 'Store' },
  { text: 'T-Shirt', active: true }
]

<Breadcrumb sections={sections} />
 <div class="ui breadcrumb">
   <a class="section">Home</a>
   <div class="divider"> / </div>
   <a class="section">Store</a>
   <div class="divider"> / </div>
   <div class="active section">T-Shirt</div>
   </div>

Icons

We could use a divider prop to define the divider type. This doesn't allow mixing divider types. Though, props are meant to be simple. The sub components are available if the user needs more control.

const sections = [
  { text: 'Home' },
  { text: 'Store' },
  { text: 'T-Shirt', active: true }
]

<Breadcrumb sections={sections} divider='right angle' />
 <div class="ui breadcrumb">
  <a class="section">Home</a>
  <i class="right angle icon divider"></i>
  <a class="section">Store</a>
  <i class="right angle icon divider"></i>
  <div class="active section">T-Shirt</div>
</div>

Links

Just as with the Breadcrumb.Section, you could provide a link, onClick, or href key to the section object to create a link style:

const sections = [
  { text: 'Home', link: true },
  { text: 'Store', onClick: handleClick },
  { text: 'T-Shirt', href: 'google.com' }
]

<Breadcrumb sections={sections} />
 <div class="ui breadcrumb">
  <a class="section">Home</a>
  <i class="divider"></i>
  <a class="section">Store</a>
  <i class="divider"></i>
  <div class="active" href='google.com'>T-Shirt</div>
</div>

Let me know what you think!

@layershifter
Copy link
Member Author

Section

I fully agree with your thoughts 👍

Divider

I agree with default value, but it may to be set custom:

<div class="divider">/</div>
<div class="divider">></div>
<Breadcrumb.Divider />
<Breadcrumb.Divider delimiter='>' />
<Breadcrumb.Divider symbol='>' />
<Breadcrumb.Divider value='>' />

May be one of variants?

Icon

I think better will be extend Icon component. What about this?

<Breadcrumb.Icon name='right chevron' />
// or
<Breadcrumb.IconDivider name='right chevron' />

Using props

I think this will be more prefered:

<Breadcrumb sections={sections} divider='>' />
<Breadcrumb sections={sections} icon-divider='right angle' />

@levithomason
Copy link
Member

I agree with default value, but it may to be set custom

I like this but think we should use children instead of a delimiter, symbol, or value prop. It will also be more flexible incase someone would like to pass another component as the divider child.

For the Icon, I like

<Breadcrumbs icon name='right angle' />

This can just be an Icon where we add the "divider" className.

I'm on mobile right now and it's a holiday here. If you'd like to start with this much, we can iterate on the rest. I'll be offline for the rest of the day.

Thanks!

@levithomason
Copy link
Member

levithomason commented Jul 4, 2016

Just noticed I copied the wrong JSX in my last comment. I meant to say for the Breadcrumb.Icon I like the idea of re-using the Icon component with this syntax:

<Breadcrumb.Icon name='right angle' />

Where this would be exposed like so:

// BreadcrumbIcon.js
import Icon from '../../elements/Icon/Icon'

function BreadcrumbIcon(props) { 
  // add the 'divider' class to the Icon
}
// Breadcrumb.js
import BreadcrumbIcon from './BreadcrumbIcon'

function Breadcrumb(props) { /* ... */ }

Breadcrumb.Icon = BreadcrumbIcon

@jhchill666
Copy link
Contributor

Seems to me inconsistent with other Apis, Button for example, eg where icon is an Icon instance or icon class name, no?

Sent from my iPhone

On 4 Jul 2016, at 19:06, Levi Thomason notifications@github.com wrote:

Just noticed I copied the wrong JSX in my last comment. I meant to say for the Breadcrumb.Icon I like the idea of re-using the Icon component with this syntax:

<Breadcrumb.Icon name='right angle' />
Where this would be exposed like so:

// BreadcrumIcon.js
import Icon from '../../elements/Icon/Icon'

function BreadcrumbIcon(props) {
// add the 'divider' class to the Icon
}
// Breadcrum.js
import BreadcrumbIcon from './BreadcrumbIcon'

function Breadcrumb(props) { /* ... */ }

Breadcrumb.Icon = BreadcrumbIcon

You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.

@levithomason
Copy link
Member

This was my original intuition as well. I've not given this one a fair amount of time or thought today. Starting tomorrow, I'll be able to look more at this. Thanks for the input and concern for the APIs.

@layershifter
Copy link
Member Author

layershifter commented Jul 5, 2016

Breadcrumb.Section

I added component that implements described functionality. But here is problem with props.

I think that only one prop (link, href, onClick) can be passed to component. I need write custom validator for this? I seen mutuallyExclusive, but it doesn't performs type check of prop.

Breadcrumb.Divider

I think about this, what about this?

<Breadcrumb.Divider />
<Breadcrumb.Divider>/</Breadcrumb.Divider>
<Breadcrumb.Divider icon='right chevron' />

<Breadcrumb sections={sections} />
<Breadcrumb sections={sections} divider='/' />
<Breadcrumb sections={sections} icon-divider='right angle' />
<Breadcrumb sections={sections} icon divider='right angle' />

@levithomason
Copy link
Member

I think that only one prop (link, href, onClick) can be passed to component.

I can see link and href being mutually exclusive, but I don't think the onClick should be. A user may want a click handler along with link/href.

I seen mutuallyExclusive, but it doesn't performs type check of prop.

This is a short coming of mutuallyExclusive, I'm extending it now to take a prop types function as its second parameter so we can use it like so:

static propTypes = {
  link: customPropTypes.mutuallyExclusive(['href'], PropTypes.bool),
  href: customPropTypes.mutuallyExclusive(['link'], PropTypes.string),
}

@levithomason
Copy link
Member

For the divider, how about this?:

<Breadcrumb.Divider />
<Breadcrumb.Divider>/</Breadcrumb.Divider>
<Breadcrumb.Divider icon='right chevron' />

<Breadcrumb sections={sections} />
<Breadcrumb sections={sections} divider='/' />
<Breadcrumb sections={sections} icon='right angle' />

@levithomason
Copy link
Member

I just merged #323 which adds the all() custom prop type. You can use it do define mutually exclusive and other types:

panels: customPropTypes.all([
  customPropTypes.mutuallyExclusive(['children']),
  PropTypes.arrayOf(PropTypes.shape({
    active: PropTypes.bool,
    title: PropTypes.string,
    content: PropTypes.string,
    onClick: PropTypes.func,
  })),
]),

if (!_.isFunction(onClick)) return

onClick()
}
Copy link
Member

Choose a reason for hiding this comment

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

Few notes on event handlers.

  1. Never preventDefault (the user can prevent default if they want)
  2. Always pass back the event
  3. propTypes validates the onClick as being a function for us, so we don't have to include validation

The refactored handleClick would look like this:

const handleClick = (e) => {
  if (onClick) onClick(e)
}

Since now all this function does is pass the event from one function to another, it can be removed entirely. We can just spread the onClick prop on the returned component. When called, it will also be called with the event.

There are times that we want to first capture a click handler. This would be when we want to pass some additional arguments after the event argument.

@levithomason
Copy link
Member

Before I forget, go ahead and check off Breadcrumb in the README.md under support.

layershifter added a commit to layershifter/stardust that referenced this pull request Jul 5, 2016
@@ -0,0 +1,48 @@
import React from 'react'
import Breadcrumb from 'src/collections/Breadcrumb/Breadcrumb'
Copy link
Member

Choose a reason for hiding this comment

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

Let's import the actual component here so we don't have a dependency on how it is exposed to the user.

import BreadcrumbSection from 'src/collections/Breadcrumb/BreadcrumbSection'

@layershifter
Copy link
Member Author

It's okay that following JSX will be generate this HTML?

<Breadcrumb.Section link>Home</Breadcrumb.Section>
 <a class="section" href="javascript:void(0)">Home</a>

May be better solution?

levithomason pushed a commit that referenced this pull request Jul 5, 2016
* Flag component

* Flag component #322

* README.md update #322

* Flag Component test update #321
@@ -6,46 +6,26 @@ import sandbox from 'test/utils/Sandbox-util'
describe('BreadcrumbSection', () => {
common.isConformant(Breadcrumb.Section)
common.rendersChildren(Breadcrumb.Section)
common.propValueOnlyToClassName(Breadcrumb.Section, 'active')
Copy link
Member Author

Choose a reason for hiding this comment

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

Can you point me? Why test is failed?

Copy link
Member

Choose a reason for hiding this comment

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

I believe you meant to use key only instead of value only:

-common.propValueOnlyToClassName(Breadcrumb.Section, 'active')
+common.propKeyOnlyToClassName(Breadcrumb.Section, 'active')

Key only will use the key name (active) as the className whereas value only will use the value. Example, color='red' uses value only, since we only the value red as the className.

Also, it is helpful to run npm run test:watch and use exclusive tests to reduce the clutter and identify issues.

Lastly, you can always click details on the ci/circle ci check on the PR to see the issue: https://circleci.com/gh/TechnologyAdvice/stardust/1315

const wrapper = mount(<Breadcrumb sections={sections} divider='>' />)
const divider = wrapper.find('BreadcrumbDivider').first()

divider.text().should.to.equal('>')
Copy link
Member

Choose a reason for hiding this comment

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

We use the chai-enzyme plugin. This allows us to move the enzyme method to the end of the assertion chain:

-divider.text().should.to.equal('>')
+divider.should.contain.text('>')

This makes for much more readable errors as well. Compare the assertion syntax here with the error messages:

shallow(<Button>foo</Button>)
  .text().should.equal('bar')

//=> expected 'foo' to equal 'bar'
shallow(<Button>foo</Button>)
  .should.contain.text('bar')

//=>  expected <Button /> to contain text 'bar', but it has 'foo'      

         HTML:

         <button type="button" class="ui button">foo</button>

@layershifter
Copy link
Member Author

I made copy for Breadcrumb from SUI docs

<ExampleSection title='Content'>
<ComponentExample
title='Divider'
description='A breadcrumb can contain a divider to show the relationship between sections, this can be
Copy link
Member

Choose a reason for hiding this comment

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

Strings in JS cannot span more than one line unless you use backticks, but this preserves whitespace including line breaks. Suggest using an array and joining the lines:

description={[
  'A breadcrumb can contain a divider to show the relationship between sections,',
  'this can be formatted as an icon or text.',
].join(' ')}

@levithomason
Copy link
Member

Wow, nice work on the docs. Very thankful for all your PRs! Left a couple comments on the docs.

I'll see what we can do to get the Icon merged quickly so we can merge this one too 👍

@layershifter
Copy link
Member Author

I think we can wait until Icon's PR will be merged 😄

@levithomason
Copy link
Member

levithomason commented Jul 11, 2016

Icon may have some more doc refactors coming. How about we update this PR and merge it to keep pace?

EDIT I'd hate to hold up this great work for a single use of the Icon :)

@layershifter
Copy link
Member Author

@levithomason I've that Icon is ready to merge, it seems that it can be today. So I think that we can wait with merge of Breadcrumb :)

@levithomason
Copy link
Member

// TODO: After update <Icon> to API replace with this code:
// return <Icon className={classes} name={icon} {...rest} />

return <Icon className={[icon, classes].join(' ')} {...rest} />
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've updated usage of Icon, but it's still failing common.implementsIconProp. It seems that this test it's not correct for such situation.

Copy link
Member

Choose a reason for hiding this comment

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

Icon/Image props need to support both strings and components being passed. We use the iconPropRenderer util for this. This case is a little more tricky because we also need to add the users classes as well as the divider class to the icon after it is rendered.

The common test was over asserting things, I've updated it. I've also pushed an update the prop renderers to support passing props. I've tested this with the new renderers and it works:

-import { customPropTypes, getUnhandledProps } from '../../utils/propUtils'
-import Icon from 'src/elements/Icon/Icon'
+import { customPropTypes, getUnhandledProps, iconPropRenderer } from '../../utils/propUtils'

/**
 * A divider sub-component for Breadcrumb component.
 */
function BreadcrumbDivider(props) {
  const {
    children, icon, className,
  } = props
  const classes = cx(
    className,
    'divider',
  )
  const rest = getUnhandledProps(BreadcrumbDivider, props)

  if (icon) {
    // TODO: After update <Icon> to API replace with this code:
    // return <Icon className={classes} name={icon} {...rest} />

-  return <Icon className={[icon, classes].join(' ')} {...rest} />
+    const iconClasses = _.isString(icon)
+      ? cx(icon, classes)
+      : cx(icon.props.className, classes)
+
+    return iconPropRenderer(icon, { ...rest, className: iconClasses })
}

Note I also used cx to build up the icon classes.

@layershifter
Copy link
Member Author

ping @levithomason

@levithomason
Copy link
Member

Hmm, I'll pull this and see what's going on.

@layershifter
Copy link
Member Author

🐺

@levithomason
Copy link
Member

Fantastic, merging!

Side note, I'm going to also extend the prop renderers to merge the className when a component is passed. This way, we don't have to check if icon/image is a string and merge classNames every time ourselves.

@levithomason levithomason merged commit 63ac495 into Semantic-Org:master Jul 17, 2016
jhchill666 pushed a commit to jhchill666/stardust that referenced this pull request Jul 19, 2016
* Flag component

* Flag component Semantic-Org#322

* README.md update Semantic-Org#322

* Flag Component test update Semantic-Org#321
jhchill666 pushed a commit to jhchill666/stardust that referenced this pull request Jul 19, 2016
* Breadcrumb component

* Breadcrumb.Section Semantic-Org#321

* Breadcrumb.Section Semantic-Org#321

* README.md update Semantic-Org#321

* (refactor) Breadcrumb.Section Semantic-Org#321

* (fix) Breadcrumb.Section test Semantic-Org#321

* (fix) Breadcrumb.Section Semantic-Org#321

* (fix) Breadcrumb.Section Semantic-Org#321

* (feat) Breadcrumb.Section Semantic-Org#321

* (fix) Breadcrumb.Divider Semantic-Org#321

* (feat) Breadcrumb Semantic-Org#321

* (fix) Breadcrumb.Section test Semantic-Org#321

* (fix) Breadcrumb.Section test Semantic-Org#321

* (fix) Breadcrumb Semantic-Org#321

* (feat) Breadcrumb Semantic-Org#321

* (fix) Breadcrumb key Semantic-Org#321

* (feat) Breadcrumb test Semantic-Org#321

* (feat) Breadcrumb tests Semantic-Org#321

* (feat) Breadcrumb docs Semantic-Org#321

* (fix) Breadcrumb docs Semantic-Org#321

* (fix) Breadcrumb Semantic-Org#321

* (fix) Breadcrumb docs

* (fix) Breadcrumb

* (Breadcrumb) Test update
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.

4 participants