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(#5): Add Button component #11

Merged
merged 14 commits into from
Jan 26, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs-app/ember-cli-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const isProduction = () => EmberApp.env() === 'production';

module.exports = function (defaults) {
let app = new EmberApp(defaults, {
autoImport: {
watchDependencies: ['@crowdstrike/ember-toucan-core'],
},
'ember-cli-babel': {
enableTypeScriptTransform: true,
},
Expand Down
5 changes: 3 additions & 2 deletions docs-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-qunit": "^7.3.1",
"fractal-page-object": "^0.3.0",
"loader.js": "^4.7.0",
"postcss": "^8.4.17",
"postcss-import": "^15.0.0",
Expand All @@ -118,20 +119,20 @@
},
"dependencies": {
"@crowdstrike/ember-oss-docs": "^1.1.0",
"@crowdstrike/ember-toucan-core": "workspace:../ember-toucan-core",
"@ember/test-waiters": "^3.0.2",
"@embroider/router": "^1.9.0",
"dompurify": "^2.4.0",
"ember-browser-services": "^4.0.3",
"ember-cached-decorator-polyfill": "^1.0.1",
"ember-modifier": "^3.2.7",
"ember-resources": "^5.4.0",
"ember-toucan-core": "workspace:../ember-toucan-core",
"highlight.js": "^11.6.0",
"highlightjs-glimmer": "^1.4.1",
"tracked-built-ins": "^3.1.0"
},
"dependenciesMeta": {
"ember-toucan-core": {
"@crowdstrike/ember-toucan-core": {
"injected": true
}
}
Expand Down
15 changes: 15 additions & 0 deletions docs/components/button/demo/base-demo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
```hbs template
<Button @onClick={{this.onClick}} @variant='secondary'>Button</Button>
Copy link
Contributor Author

@ynotdraw ynotdraw Jan 21, 2023

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:

  • Release this as a package
  • Update ember-oss-docs to use this repo

A bit of a πŸ” and πŸ₯š problem

```

```js component
import Component from '@glimmer/component';
import { action } from '@ember/object';

export default class extends Component {
@action
onClick(e) {
alert('Button clicked!');
}
}
```
152 changes: 152 additions & 0 deletions docs/components/button/index.md
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.
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 am not a writer, so appreciate any feedback in this entire document. I did my best tho ☺️

Copy link
Contributor

Choose a reason for hiding this comment

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

sounds good to me πŸ™ƒ

Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor Author

@ynotdraw ynotdraw Jan 21, 2023

Choose a reason for hiding this comment

The 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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>
1 change: 0 additions & 1 deletion docs/demos/demo-a.md

This file was deleted.

13 changes: 12 additions & 1 deletion ember-toucan-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@
},
"peerDependencies": {
"@crowdstrike/ember-toucan-styles": "^1.0.5",
"@ember/test-helpers": "^2.8.1",
"@glimmer/tracking": "^1.1.2",
"autoprefixer": "^10.0.2",
"ember-source": "^4.8.0",
"fractal-page-object": "^0.3.0",
"postcss": "^8.2.14",
"tailwindcss": "^2.2.15 || ^3.0.0"
},
"dependencies": {
"@babel/runtime": "^7.20.7",
"@embroider/addon-shim": "^1.0.0"
},
"devDependencies": {
Expand All @@ -50,6 +53,7 @@
"@babel/plugin-syntax-decorators": "^7.17.0",
"@babel/preset-typescript": "^7.18.6",
"@crowdstrike/ember-toucan-styles": "^1.0.5",
"@ember/test-helpers": "^2.8.1",
"@embroider/addon-dev": "^2.0.0",
"@glimmer/component": "^1.1.2",
"@glimmer/tracking": "^1.1.2",
Expand Down Expand Up @@ -88,6 +92,7 @@
"eslint-plugin-n": "^15.6.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.0.0",
"fractal-page-object": "^0.3.0",
"postcss": "^8.2.14",
"prettier": "^2.8.3",
"prettier-plugin-ember-template-tag": "^0.3.0",
Expand All @@ -107,18 +112,24 @@
"version": 2,
"type": "addon",
"main": "addon-main.cjs",
"app-js": {}
"app-js": {
"./components/button/index.js": "./dist/_app_/components/button/index.js"
}
},
"exports": {
".": "./dist/index.js",
"./*": {
"types": "./dist/*.d.ts",
"default": "./dist/*.js"
},
"./test-support": "./dist/test-support/index.js",
"./addon-main.js": "./addon-main.cjs"
},
"typesVersions": {
"*": {
"test-support": [
"./dist/test-support/index.d.ts"
],
"*": [
"dist/*"
]
Expand Down
1 change: 1 addition & 0 deletions ember-toucan-core/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default {
'components/**/*.js',
'index.js',
'template-registry.js',
'test-support/index.js',
]),

// These are the modules that should get reexported into the traditional
Expand Down
19 changes: 19 additions & 0 deletions ember-toucan-core/src/components/button/index.hbs
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>
89 changes: 89 additions & 0 deletions ember-toucan-core/src/components/button/index.ts
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);
}
}
5 changes: 3 additions & 2 deletions ember-toucan-core/src/template-registry.ts
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;
}
Loading