Skip to content

Commit

Permalink
feat: checkbox component (#201)
Browse files Browse the repository at this point in the history
* feat(checkbox): draft checkbox component

* docs(checkbox): add storybook files for checkbox and checkbox group

* test(checkbox): add test files for checkbox and checkbox group

* refactor(checkbox): refactor for checkbox and checkbox group

* fix(checkbox): make label property optional

* fix(checkbox): delete checkbox group element and allow single checkbox to be indeterminate

* style(checkbox): fix css prettier issue

* refactor(checkbox): some logic simplified

* refactor(checkbox): remove unneeded style

* refactor(input): lint fix

* fix(checkbox): disabled color corrected

* fix(checkbox): prevent user selection for checkbox label

* feat(checkbox): draft checkbox component

* docs(checkbox): add storybook files for checkbox and checkbox group

* test(checkbox): add test files for checkbox and checkbox group

* refactor(checkbox): refactor for checkbox and checkbox group

* fix(checkbox): make label property optional

* fix(checkbox): delete checkbox group element and allow single checkbox to be indeterminate

* style(checkbox): fix css prettier issue

* refactor(checkbox): some logic simplified

* refactor(checkbox): remove unneeded style

* refactor(input): lint fix

* fix(checkbox): disabled color corrected

* fix(checkbox): prevent user selection for checkbox label

* fix(input): lint fix

* fix(checkbox): cursor pointer for label and checkbox

* feat(checkbox): indeterminate state handling added

* docs(checkbox): indeterminate and checked state docs added

Co-authored-by: Ali Balbars <[email protected]>
Co-authored-by: Murat Çorlu <[email protected]>
Co-authored-by: Murat Çorlu <[email protected]>
Co-authored-by: olkeoguz <[email protected]>
Co-authored-by: Talha Dogrul <[email protected]>
  • Loading branch information
6 people authored Sep 12, 2022
1 parent 6a7d747 commit a940f3f
Show file tree
Hide file tree
Showing 6 changed files with 344 additions and 0 deletions.
1 change: 1 addition & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module.exports = {
'tab',
'tooltip',
'progress-indicator',
'checkbox',
'alert'
],
],
Expand Down
1 change: 1 addition & 0 deletions src/baklava.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export { default as BlTabGroup } from './components/tab-group/bl-tab-group';
export { default as BlTabPanel } from './components/tab-group/tab-panel/bl-tab-panel';
export { default as BlTooltip } from './components/tooltip/bl-tooltip';
export { default as BlProgressIndicator } from './components/progress-indicator/bl-progress-indicator';
export { default as BlCheckbox } from './components/checkbox/bl-checkbox';
export { default as BlAlert } from './components/alert/bl-alert';
export { getIconPath, setIconPath } from './utilities/asset-paths';
50 changes: 50 additions & 0 deletions src/components/checkbox/bl-checkbox.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
:host {
display: inline-block;
vertical-align: middle;
}

label {
display: flex;
align-items: center;
gap: var(--bl-size-2xs);
color: var(--bl-color-secondary);
font: var(--bl-font-title-3);
cursor: pointer;
user-select: none;
}

input {
appearance: none;
position: absolute;
}

.check-mark {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: var(--bl-size-m);
height: var(--bl-size-m);
border: 1px solid var(--bl-color-border);
border-radius: var(--bl-border-radius-xs);
color: var(--bl-color-primary-background);
font-size: var(--bl-font-size-2xs);
}

:host([checked]) .label {
color: var(--bl-color-primary);
}

:host(:is([checked], [indeterminate])) .check-mark {
background-color: var(--bl-color-primary);
border: none;
}

:host([disabled]) .check-mark,
:host([disabled]) .label {
color: var(--bl-color-content-passive);
}

:host([disabled]) .check-mark {
background-color: var(--bl-color-secondary-background);
}
106 changes: 106 additions & 0 deletions src/components/checkbox/bl-checkbox.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
import { Meta, Canvas, ArgsTable, Story } from '@storybook/addon-docs';

<Meta
title="Components/Checkbox"
component="bl-checkbox"
argTypes={{
label: {
control: 'text'
},
disabled: {
control: 'boolean',
default: false
},
checked: {
control: 'boolean',
default: false
},
indeterminate: {
control: 'boolean',
default: false
}
}}
/>

export const CheckboxTemplate = (args) => html`
<bl-checkbox
?disabled=${args.disabled}
?checked=${args.checked}
?indeterminate=${args.indeterminate}>${args.label}</bl-checkbox>
`;

# Checkbox
Checkbox component can be used to control checked / unchecked statuses.

### Usage

Use checkbox component for getting true/false input from users.

* Don't use checkbox as an action button.
* Checkbox label is not required but if you want to use checbox without a label, set `aria-label` attribute.

## Basic

You can show label by just using slot.

<Canvas>
<Story name="Basic Usage" args={{ label: 'Label' }}>
{CheckboxTemplate.bind({})}
</Story>
</Canvas>

## Checked

Checked state can be set via `checked` attribute.

<Canvas>
<Story name="Checked" args={{ label: 'checkbox', checked: true }}>
{CheckboxTemplate.bind({})}
</Story>
</Canvas>

## Indeterminate

Indeterminate state is regardless with `checked` state. A checkbox can be both `checked` and `indeterminate` at the
same time.Indeterminate state is mainly used for parent/child selections while parent checkbox represents if all of
the child will/should be checked or not. User interaction with checkbox (if checkbox is not disabled) takes checkbox
from `indeterminate` state. Checkbox doesn't go this state with a user interaction.

<Canvas>
<Story name="Indeterminate" args={{ label: 'checkbox', indeterminate: true }}>
{CheckboxTemplate.bind({})}
</Story>
</Canvas>

## Indeterminate And Checked

Indeterminate state is regardless with `checked` state. A checkbox cannot be both `checked` and `indeterminate` at the
same time. Unless there is a user interaction, when `indeterminate` state is active `checked` state changes ignored.

<Canvas>
<Story name="Indeterminate And Checked" args={{ label: 'checkbox', indeterminate: true, checked: true }}>
{CheckboxTemplate.bind({})}
</Story>
</Canvas>

## Disabled

Disabled state can be set via `disabled` attribute. A checkbox can be `disabled` and `checked` (and even `indeterminate`) at the same time.

<Canvas>
<Story name="Disabled" args={{ label: 'Disabled', disabled: true }}>
{CheckboxTemplate.bind({})}
</Story>
<Story name="Disabled and Checked" args={{ label: 'Disabled and Checked', disabled: true, checked: true }}>
{CheckboxTemplate.bind({})}
</Story>
<Story name="Disabled, Checked and Indeterminate" args={{ label: 'Disabled, Checked and Indeterminate', disabled: true, checked: true, indeterminate: true }}>
{CheckboxTemplate.bind({})}
</Story>
</Canvas>

## Reference

<ArgsTable of="bl-checkbox" />
104 changes: 104 additions & 0 deletions src/components/checkbox/bl-checkbox.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { assert, fixture, html, elementUpdated, expect, oneEvent } from '@open-wc/testing';
import BlCheckbox from './bl-checkbox';

describe('bl-checkbox', () => {
it('should be defined checkbox instance', () => {
const el = document.createElement('bl-checkbox');
assert.instanceOf(el, BlCheckbox);
});

it('should be rendered with default values', async () => {
const el = await fixture(html`<bl-checkbox></bl-checkbox>`);

assert.shadowDom.equal(
el,
`
<label>
<input type="checkbox" name="checkbox" />
<div class="check-mark"></div>
<span class="label"><slot></slot></span>
</label>
`
);
});

it('should be rendered with correct label attribute', async () => {
const el = await fixture(html`<bl-checkbox label="test label"></bl-checkbox>`);

expect(el.shadowRoot?.querySelector('span')).to.exist;
expect(el.getAttribute('label')).to.eq('test label');
});

it('should be rendered with correct label attribute when label attribute was changed', async () => {
const el = await fixture(html`<bl-checkbox label="test label"></bl-checkbox>`);

el.setAttribute('label', 'new test label');

await elementUpdated(el);

expect(el.getAttribute('label')).to.eq('new test label');
});

it('should be rendered with check icon when checkbox checked', async () => {
const el = await fixture(html`<bl-checkbox checked></bl-checkbox>`);
const iconEl = el.shadowRoot?.querySelector('bl-icon');

expect(iconEl?.getAttribute('name')).to.eq('check');
});

it('should render with `checked` attribute as checked value', async () => {
const el = await fixture(html`<bl-checkbox checked></bl-checkbox>`);
expect(el.shadowRoot?.querySelector('input')?.checked).to.eq(true);
});

describe('attributes', () => {
it('should render with `disabled` attribute as disabled', async () => {
const el = await fixture(html`<bl-checkbox disabled></bl-checkbox>`);
expect(el.shadowRoot?.querySelector('input')?.hasAttribute('disabled')).to.eq(true);
});

it('should not render with `indeterminate` attribute as indeterminate', async () => {
const el = await fixture(html`<bl-checkbox indeterminate></bl-checkbox>`);
expect(el.shadowRoot?.querySelector('input')?.hasAttribute('indeterminate')).to.eq(false);
});
});

describe('update', () => {
it('should set checked to false when indeterminate set to true', async () => {
const el = await fixture(html`<bl-checkbox checked></bl-checkbox>`);

el.setAttribute('indeterminate', 'true');
await elementUpdated(el);

expect(el.hasAttribute('checked')).to.eq(false);
});
it('should set checked to false when indeterminate and checked set to true at start', async () => {
const el = await fixture(html`<bl-checkbox indeterminate checked></bl-checkbox>`);
expect(el.hasAttribute('checked')).to.eq(false);
});
});

describe('events', () => {
it('should fire bl-checkbox-change event with detail is true when checkbox is unchecked', async () => {
const el = await fixture(html`<bl-checkbox></bl-checkbox>`);
const checkbox = el.shadowRoot?.querySelector('input');

setTimeout(() => checkbox?.click());
const ev = await oneEvent(el, 'bl-checkbox-change');

expect(ev).to.exist;
expect(ev.detail).to.be.equal(true);
});

it('should fire bl-checkbox-change event with detail is false when checkbox is checked', async () => {
const el = await fixture(html`<bl-checkbox checked></bl-checkbox>`);
const checkbox = el.shadowRoot?.querySelector('input');

setTimeout(() => checkbox?.click());
const ev = await oneEvent(el, 'bl-checkbox-change');

expect(ev).to.exist;
expect(ev.detail).to.be.equal(false);
});
});
});
82 changes: 82 additions & 0 deletions src/components/checkbox/bl-checkbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { CSSResultGroup, html, LitElement, TemplateResult } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { live } from 'lit/directives/live.js';
import { event, EventDispatcher } from '../../utilities/event';
import '../icon/bl-icon';
import style from './bl-checkbox.css';

/**
* @tag bl-checkbox
* @summary Baklava Checkbox component
*/
@customElement('bl-checkbox')
export default class BlCheckbox extends LitElement {
static get styles(): CSSResultGroup {
return [style];
}

/**
* Sets the checked state for checkbox
*/
@property({ type: Boolean, reflect: true })
checked = false;

/**
* Sets the disabled state for checkbox
*/
@property({ type: Boolean, reflect: true })
disabled = false;

/**
* Sets the indeterminate state for checkbox
*/
@property({ type: Boolean, reflect: true })
indeterminate = false;

/**
* Fires whenever user change the value of the checkbox.
*/
@event('bl-checkbox-change') private onChange: EventDispatcher<boolean>;

handleChange(event: CustomEvent) {
const target = event.target as HTMLInputElement;
this.checked = target.checked;
this.onChange(target.checked);
this.indeterminate = false;
}

update(changedProperties: Map<string, unknown>) {
super.update(changedProperties);
if (this.indeterminate && this.checked) {
this.checked = false;
this.requestUpdate('checked', true);
}
}

render(): TemplateResult {
let icon = '';
if (this.checked) icon = 'check';
if (this.indeterminate) icon = 'minus';

return html`
<label>
<input
type="checkbox"
name="checkbox"
.checked=${live(this.checked)}
?disabled=${this.disabled}
.indeterminate=${this.indeterminate}
@change=${this.handleChange}
/>
<div class="check-mark">${icon ? html`<bl-icon name="${icon}"></bl-icon>` : null}</div>
<span class="label"><slot></slot></span>
</label>
`;
}
}

declare global {
interface HTMLElementTagNameMap {
'bl-checkbox': BlCheckbox;
}
}

0 comments on commit a940f3f

Please sign in to comment.