From a940f3fbb4fbb07c467c9c98c76a7ab7ff4d0804 Mon Sep 17 00:00:00 2001 From: Ali Balbars Date: Mon, 12 Sep 2022 11:28:24 +0300 Subject: [PATCH] feat: checkbox component (#201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: Murat Çorlu Co-authored-by: Murat Çorlu <127687+muratcorlu@users.noreply.github.com> Co-authored-by: olkeoguz Co-authored-by: Talha Dogrul --- commitlint.config.js | 1 + src/baklava.ts | 1 + src/components/checkbox/bl-checkbox.css | 50 +++++++++ .../checkbox/bl-checkbox.stories.mdx | 106 ++++++++++++++++++ src/components/checkbox/bl-checkbox.test.ts | 104 +++++++++++++++++ src/components/checkbox/bl-checkbox.ts | 82 ++++++++++++++ 6 files changed, 344 insertions(+) create mode 100644 src/components/checkbox/bl-checkbox.css create mode 100644 src/components/checkbox/bl-checkbox.stories.mdx create mode 100644 src/components/checkbox/bl-checkbox.test.ts create mode 100644 src/components/checkbox/bl-checkbox.ts diff --git a/commitlint.config.js b/commitlint.config.js index d425a0ae..ac868546 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -15,6 +15,7 @@ module.exports = { 'tab', 'tooltip', 'progress-indicator', + 'checkbox', 'alert' ], ], diff --git a/src/baklava.ts b/src/baklava.ts index fa334537..2a857d82 100644 --- a/src/baklava.ts +++ b/src/baklava.ts @@ -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'; diff --git a/src/components/checkbox/bl-checkbox.css b/src/components/checkbox/bl-checkbox.css new file mode 100644 index 00000000..24ca9eb2 --- /dev/null +++ b/src/components/checkbox/bl-checkbox.css @@ -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); +} diff --git a/src/components/checkbox/bl-checkbox.stories.mdx b/src/components/checkbox/bl-checkbox.stories.mdx new file mode 100644 index 00000000..f602bb61 --- /dev/null +++ b/src/components/checkbox/bl-checkbox.stories.mdx @@ -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'; + + + +export const CheckboxTemplate = (args) => html` +${args.label} +`; + +# 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. + + + + {CheckboxTemplate.bind({})} + + + +## Checked + +Checked state can be set via `checked` attribute. + + + + {CheckboxTemplate.bind({})} + + + +## 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. + + + + {CheckboxTemplate.bind({})} + + + +## 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. + + + + {CheckboxTemplate.bind({})} + + + +## Disabled + +Disabled state can be set via `disabled` attribute. A checkbox can be `disabled` and `checked` (and even `indeterminate`) at the same time. + + + + {CheckboxTemplate.bind({})} + + + {CheckboxTemplate.bind({})} + + + {CheckboxTemplate.bind({})} + + + +## Reference + + diff --git a/src/components/checkbox/bl-checkbox.test.ts b/src/components/checkbox/bl-checkbox.test.ts new file mode 100644 index 00000000..ec1b80d0 --- /dev/null +++ b/src/components/checkbox/bl-checkbox.test.ts @@ -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``); + + assert.shadowDom.equal( + el, + ` + + ` + ); + }); + + it('should be rendered with correct label attribute', async () => { + const el = await fixture(html``); + + 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``); + + 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``); + 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``); + 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``); + 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``); + 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``); + + 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``); + 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``); + 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``); + 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); + }); + }); +}); diff --git a/src/components/checkbox/bl-checkbox.ts b/src/components/checkbox/bl-checkbox.ts new file mode 100644 index 00000000..2866dddd --- /dev/null +++ b/src/components/checkbox/bl-checkbox.ts @@ -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; + + handleChange(event: CustomEvent) { + const target = event.target as HTMLInputElement; + this.checked = target.checked; + this.onChange(target.checked); + this.indeterminate = false; + } + + update(changedProperties: Map) { + 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` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'bl-checkbox': BlCheckbox; + } +}