-
Notifications
You must be signed in to change notification settings - Fork 6
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(#5): Add Button component #11
Changes from 10 commits
d736e0b
512c125
6587220
fb0e1d1
c93cf6a
206b2e6
ad93eb5
eec254f
e6d76b2
b6738a7
962819e
f43bb59
125db0a
297aa7c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
```hbs template | ||
<Button @onClick={{this.onClick}} @variant='secondary'>Button</Button> | ||
``` | ||
|
||
```js component | ||
import Component from '@glimmer/component'; | ||
import { action } from '@ember/object'; | ||
|
||
export default class extends Component { | ||
@action | ||
onClick(e) { | ||
alert('Button clicked!'); | ||
} | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
# Button | ||
|
||
Buttons are clickable elements used primarily for actions. Button content expresses what action will occur when the user interacts with it. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not a writer, so appreciate any feedback in this entire document. I did my best tho There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sounds good to me π There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unlike our internal docs, which focus on all disciplines at once, these "core" docs, are primarily focused on developers. |
||
|
||
## Variants | ||
|
||
You can customize the appearance of the button with the `@variant` component argument. | ||
|
||
<div class="flex gap-x-4"> | ||
<Button @variant="primary">Primary</Button> | ||
<Button @variant="secondary">Secondary</Button> | ||
<Button @variant="destructive">Destructive</Button> | ||
<Button @variant="link">Link</Button> | ||
<Button @variant="quiet">Quiet</Button> | ||
<Button @variant="bare">Bare</Button> | ||
</div> | ||
|
||
## Handling Clicks | ||
|
||
To handle click events use the `@onClick` component argument. | ||
|
||
```hbs | ||
<Button @onClick={{this.handleClick}}>Click Me</Button> | ||
``` | ||
|
||
## Disabled State | ||
|
||
`aria-disabled` is used over the `disabled` attribute so that screenreaders can still focus the element. To set the button as disabled, use `@isDisabled`. | ||
|
||
```hbs | ||
<Button @isDisabled={{true}}>Disabled</Button> | ||
``` | ||
|
||
A disabled named block is provided so that users can optionally render additional content when the button is disabled. | ||
|
||
```hbs | ||
<Button @isDisabled={{true}}> | ||
<:disabled> | ||
<svg | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The icon I used in this example is from an MIT-licensed open source library I built. We can replace it at any time, as we see fit. |
||
class='h-4 w-4' | ||
xmlns='http://www.w3.org/2000/svg' | ||
width='24' | ||
height='24' | ||
stroke='currentColor' | ||
viewBox='0 0 24 24' | ||
> | ||
<path | ||
d='M18.644 21h-13.2a.945.945 0 01-1-1v-7.2a.945.945 0 011-1h13.1a.945.945 0 011 1V20a.808.808 0 01-.225.725.966.966 0 01-.675.275zm-10.9-9.2V7.3a4.3 4.3 0 118.6 0v4.5m-4.3 3.7v2' | ||
fill='none' | ||
stroke-linecap='round' | ||
stroke-linejoin='round' | ||
stroke-width='2' | ||
/> | ||
</svg> | ||
</:disabled> | ||
<:default> | ||
Disabled | ||
</:default> | ||
</Button> | ||
``` | ||
|
||
<div class="flex gap-x-4"> | ||
{{#each (array "primary" "secondary" "destructive" "link" "quiet" "bare") as |variant|}} | ||
<Button @variant={{variant}} @isDisabled={{true}}> | ||
<:disabled> | ||
<svg | ||
class='h-4 w-4' | ||
xmlns='http://www.w3.org/2000/svg' | ||
width='24' | ||
height='24' | ||
stroke='currentColor' | ||
viewBox='0 0 24 24' | ||
> | ||
<path | ||
d="M18.644 21h-13.2a.945.945 0 01-1-1v-7.2a.945.945 0 011-1h13.1a.945.945 0 011 1V20a.808.808 0 01-.225.725.966.966 0 01-.675.275zm-10.9-9.2V7.3a4.3 4.3 0 118.6 0v4.5m-4.3 3.7v2" | ||
fill='none' | ||
stroke-linecap='round' | ||
stroke-linejoin='round' | ||
stroke-width='2' | ||
/> | ||
</svg> | ||
</:disabled> | ||
<:default> | ||
{{variant}} | ||
</:default> | ||
</Button> | ||
{{/each}} | ||
</div> | ||
|
||
## Loading State | ||
|
||
Button exposes an `@isLoading` component argument. The button content will be only visible to screenreaders. | ||
|
||
```hbs | ||
<Button @isLoading={{true}}>Loadingβ¦</Button> | ||
``` | ||
|
||
A loading named block is also provided for providing custom loading content. | ||
|
||
```hbs | ||
<Button @isLoading={{true}}> | ||
<:loading> | ||
<svg | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as above in regards to icons here |
||
class='h-4 w-4 animate-spin' | ||
xmlns='http://www.w3.org/2000/svg' | ||
width='24' | ||
height='24' | ||
stroke='currentColor' | ||
viewBox='0 0 24 24' | ||
> | ||
<path | ||
d='M5.95 5.7L7 6.75 8.05 7.8m8.4 8.4l.95.95.95.95m.2-12.4L17.5 6.75 16.45 7.8M6.35 12h-3.1m17.5 0h-2.6m-5.9 9v-3.1m0-14.9v3.1' | ||
fill='none' | ||
stroke-linecap='round' | ||
stroke-linejoin='round' | ||
stroke-width='2' | ||
/> | ||
</svg> | ||
</:loading> | ||
<:default> | ||
Loading⦠| ||
</:default> | ||
</Button> | ||
``` | ||
|
||
<div class="flex gap-x-4"> | ||
{{#each (array "primary" "secondary" "destructive" "link" "quiet" "bare") as |variant|}} | ||
<Button @variant={{variant}} @isLoading={{true}}> | ||
<:loading> | ||
<svg | ||
class='h-4 w-4 animate-spin' | ||
xmlns="http://www.w3.org/2000/svg" | ||
width="24" | ||
height="24" | ||
stroke="currentColor" | ||
viewBox="0 0 24 24" | ||
> | ||
<path | ||
d="M5.95 5.7L7 6.75 8.05 7.8m8.4 8.4l.95.95.95.95m.2-12.4L17.5 6.75 16.45 7.8M6.35 12h-3.1m17.5 0h-2.6m-5.9 9v-3.1m0-14.9v3.1" | ||
fill="none" | ||
stroke-linecap="round" | ||
stroke-linejoin="round" | ||
stroke-width="2" | ||
/> | ||
</svg> | ||
</:loading> | ||
<:default> | ||
{{variant}} | ||
</:default> | ||
</Button> | ||
{{/each}} | ||
</div> |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<button | ||
aria-disabled={{if @isDisabled "true"}} | ||
class={{this.styles}} | ||
type="button" | ||
{{on "click" this.onClick}} | ||
...attributes | ||
> | ||
{{#if @isLoading}} | ||
{{yield to="loading"}} | ||
<span class="sr-only" data-loading>{{yield}}</span> | ||
{{else if @isDisabled}} | ||
<span class="flex items-center gap-x-2"> | ||
{{yield}} | ||
{{yield to="disabled"}} | ||
</span> | ||
{{else}} | ||
{{yield}} | ||
{{/if}} | ||
</button> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import Component from '@glimmer/component'; | ||
import { assert } from '@ember/debug'; | ||
import { action } from '@ember/object'; | ||
|
||
const VALID_VARIANTS = [ | ||
'bare', | ||
'destructive', | ||
'link', | ||
'primary', | ||
'quiet', | ||
'secondary', | ||
] as const; | ||
|
||
export type ButtonVariant = (typeof VALID_VARIANTS)[number]; | ||
|
||
const STYLES = { | ||
base: [ | ||
'focusable', | ||
'inline-flex', | ||
'items-center', | ||
'justify-center', | ||
'rounded-sm', | ||
'transition', | ||
'truncate', | ||
'type-md-medium', | ||
], | ||
variants: { | ||
bare: ['focusable'], | ||
destructive: ['focusable-destructive', 'interactive-destructive'], | ||
link: ['font-normal', 'interactive-link', 'underline'], | ||
primary: ['interactive-primary'], | ||
quiet: ['font-normal', 'interactive-quiet'], | ||
secondary: ['interactive-normal'], | ||
}, | ||
}; | ||
|
||
export interface ButtonSignature { | ||
Args: { | ||
isDisabled?: boolean; | ||
isLoading?: boolean; | ||
onClick?: (event: MouseEvent) => void; | ||
variant?: ButtonVariant; | ||
}; | ||
Blocks: { default: []; disabled: []; loading: [] }; | ||
Element: HTMLButtonElement; | ||
} | ||
|
||
export default class Button extends Component<ButtonSignature> { | ||
get variant() { | ||
const { variant } = this.args; | ||
|
||
assert( | ||
`Invalid variant for Button: '${variant}' (allowed values: [${VALID_VARIANTS.join( | ||
', ' | ||
)}])`, | ||
VALID_VARIANTS.includes(variant ?? 'primary') | ||
); | ||
|
||
return variant || 'primary'; | ||
} | ||
|
||
get styles() { | ||
if (this.variant === 'bare') { | ||
return STYLES.variants.bare.join(' '); | ||
} | ||
|
||
const buttonStyles = [...STYLES.base, ...STYLES.variants[this.variant]]; | ||
const disabledStyles = ['interactive-disabled', 'focus:outline-none']; | ||
|
||
if (this.variant !== 'link') { | ||
buttonStyles.push('px-4', 'py-1'); | ||
} | ||
|
||
return this.args.isDisabled | ||
? [...buttonStyles, ...disabledStyles].join(' ') | ||
: buttonStyles.join(' '); | ||
} | ||
|
||
@action | ||
onClick(event: MouseEvent) { | ||
if (this.args.isDisabled) { | ||
event.stopImmediatePropagation(); | ||
simonihmig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return; | ||
} | ||
|
||
this.args.onClick?.(event); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import type ButtonComponent from './components/button'; | ||
|
||
export default interface Registry { | ||
// TODO: put components here | ||
Button: unknown; | ||
Button: typeof ButtonComponent; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This Button instance actually has a collision with the Button component from
@crowdstrike/ember-oss-docs
. For now, we can continue to write docs, but be aware you may hit this if there are components named the same there.The eventual goal is to:
ember-oss-docs
to use this repoA bit of a π and π₯ problem