diff --git a/.changeset/pf-checkbox.md b/.changeset/pf-checkbox.md new file mode 100644 index 0000000000..68f48a7617 --- /dev/null +++ b/.changeset/pf-checkbox.md @@ -0,0 +1,23 @@ +--- +"@patternfly/elements": minor +--- +✨ Added `` + +```html +
+ + + + + + Submit +
+``` diff --git a/elements/package.json b/elements/package.json index 0cd47fe88f..97d6e45901 100644 --- a/elements/package.json +++ b/elements/package.json @@ -25,6 +25,7 @@ "./pf-button/pf-button.js": "./pf-button/pf-button.js", "./pf-card/BaseCard.js": "./pf-card/BaseCard.js", "./pf-card/pf-card.js": "./pf-card/pf-card.js", + "./pf-checkbox/pf-checkbox.js": "./pf-checkbox/pf-checkbox.js", "./pf-clipboard-copy/BaseClipboardCopy.js": "./pf-clipboard-copy/BaseClipboardCopy.js", "./pf-clipboard-copy/pf-clipboard-copy.js": "./pf-clipboard-copy/pf-clipboard-copy.js", "./pf-code-block/BaseCodeBlock.js": "./pf-code-block/BaseCodeBlock.js", diff --git a/elements/pf-checkbox/README.md b/elements/pf-checkbox/README.md new file mode 100644 index 0000000000..f4c234c411 --- /dev/null +++ b/elements/pf-checkbox/README.md @@ -0,0 +1,6 @@ +# Checkbox +A **checkbox** is used to select a single item or multiple items, typically to choose elements to perform an action or to reflect a binary setting. + +```html + +``` diff --git a/elements/pf-checkbox/demo/controlled.html b/elements/pf-checkbox/demo/controlled.html new file mode 100644 index 0000000000..94694b1a34 --- /dev/null +++ b/elements/pf-checkbox/demo/controlled.html @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/elements/pf-checkbox/demo/disabled.html b/elements/pf-checkbox/demo/disabled.html new file mode 100644 index 0000000000..65fbc1145f --- /dev/null +++ b/elements/pf-checkbox/demo/disabled.html @@ -0,0 +1,7 @@ + + + + + diff --git a/elements/pf-checkbox/demo/form-association.html b/elements/pf-checkbox/demo/form-association.html new file mode 100644 index 0000000000..dbb48def76 --- /dev/null +++ b/elements/pf-checkbox/demo/form-association.html @@ -0,0 +1,41 @@ + +
+ + + + + + Submit and Log Results +
+ FormData + +
+
+ + + + diff --git a/elements/pf-checkbox/demo/label.html b/elements/pf-checkbox/demo/label.html new file mode 100644 index 0000000000..c1a24385fc --- /dev/null +++ b/elements/pf-checkbox/demo/label.html @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/elements/pf-checkbox/demo/nested.html b/elements/pf-checkbox/demo/nested.html new file mode 100644 index 0000000000..74b1b9d0f7 --- /dev/null +++ b/elements/pf-checkbox/demo/nested.html @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/elements/pf-checkbox/demo/pf-checkbox.html b/elements/pf-checkbox/demo/pf-checkbox.html new file mode 100644 index 0000000000..ad3e1b965f --- /dev/null +++ b/elements/pf-checkbox/demo/pf-checkbox.html @@ -0,0 +1,6 @@ + + + + diff --git a/elements/pf-checkbox/demo/validation.html b/elements/pf-checkbox/demo/validation.html new file mode 100644 index 0000000000..049b3f8e07 --- /dev/null +++ b/elements/pf-checkbox/demo/validation.html @@ -0,0 +1,10 @@ + + + + + diff --git a/elements/pf-checkbox/demo/with-body.html b/elements/pf-checkbox/demo/with-body.html new file mode 100644 index 0000000000..b30fe29e84 --- /dev/null +++ b/elements/pf-checkbox/demo/with-body.html @@ -0,0 +1,9 @@ + + This is where custom content goes + + + + + diff --git a/elements/pf-checkbox/demo/with-description-and-body.html b/elements/pf-checkbox/demo/with-description-and-body.html new file mode 100644 index 0000000000..8c7278b118 --- /dev/null +++ b/elements/pf-checkbox/demo/with-description-and-body.html @@ -0,0 +1,11 @@ + + This is where custom content goes + Single-tenant cloud service hosted and managed by Red Hat that offers high-availability enterprise-grade clusters in a virtual private cloud on AWS or GCP. + + + + + + diff --git a/elements/pf-checkbox/demo/with-description.html b/elements/pf-checkbox/demo/with-description.html new file mode 100644 index 0000000000..81dbe1d7d0 --- /dev/null +++ b/elements/pf-checkbox/demo/with-description.html @@ -0,0 +1,9 @@ + + Single-tenant cloud service hosted and managed by Red Hat that offers high-availability enterprise-grade clusters in a virtual private cloud on AWS or GCP. + + + + + diff --git a/elements/pf-checkbox/docs/pf-checkbox.md b/elements/pf-checkbox/docs/pf-checkbox.md new file mode 100644 index 0000000000..c0b1ea8209 --- /dev/null +++ b/elements/pf-checkbox/docs/pf-checkbox.md @@ -0,0 +1,17 @@ +{% renderOverview %} + +{% endrenderOverview %} + +{% band header="Usage" %}{% endband %} + +{% renderSlots %}{% endrenderSlots %} + +{% renderAttributes %}{% endrenderAttributes %} + +{% renderMethods %}{% endrenderMethods %} + +{% renderEvents %}{% endrenderEvents %} + +{% renderCssCustomProperties %}{% endrenderCssCustomProperties %} + +{% renderCssParts %}{% endrenderCssParts %} diff --git a/elements/pf-checkbox/pf-checkbox.css b/elements/pf-checkbox/pf-checkbox.css new file mode 100644 index 0000000000..09ea97ad75 --- /dev/null +++ b/elements/pf-checkbox/pf-checkbox.css @@ -0,0 +1,64 @@ +:host { + --pf-c-check--GridGap: var(--pf-global--spacer--xs, 0.25rem) var(--pf-global--spacer--sm, 0.5rem); + --pf-c-check__label--disabled--Color: var(--pf-global--disabled-color--100, #6a6e73); + --pf-c-check__label--Color: var(--pf-global--Color--100, #151515); + --pf-c-check__label--FontWeight: var(--pf-global--FontWeight--normal, 400); + --pf-c-check__label--FontSize: var(--pf-global--FontSize--md, 1rem); + --pf-c-check__label--LineHeight: var(--pf-global--LineHeight--sm, 1.3); + --pf-c-check__input--Height: var(--pf-c-check__label--FontSize); + --pf-c-check__input--MarginTop: calc(((var(--pf-c-check__label--FontSize) * var(--pf-c-check__label--LineHeight)) - var(--pf-c-check__input--Height)) / 2); + --pf-c-check__description--FontSize: var(--pf-global--FontSize--sm, 0.875rem); + --pf-c-check__description--Color: var(--pf-global--Color--200, #6a6e73); + --pf-c-check__body--MarginTop: var(--pf-global--spacer--sm, 0.5rem); + --pf-c-check__label-required--MarginLeft: var(--pf-global--spacer--xs, 0.25rem); + --pf-c-check__label-required--FontSize: var(--pf-global--FontSize--sm, 0.875rem); + --pf-c-check__label-required--Color: var(--pf-global--danger-color--100, #c9190b); + display: grid; + grid-template-columns: auto 1fr; + grid-gap: var(--pf-c-check--GridGap); + align-items: start; + justify-items: start; +} + +[hidden] { display: none !important; } + +#nested::slotted(*) { + margin-inline-start: var(--pf-global--spacer--md, 1rem); + grid-column: -1/1; +} + +#description { + display: inline; + grid-column: 2; + font-size: var(--pf-c-check__description--FontSize); + color: var(--pf-c-check__description--Color); +} + +#body { + display: inline; + grid-column: 2; + margin-top: var(--pf-c-check__body--MarginTop); +} + +input { + height: var(--pf-c-check__input--Height); + margin-block-start: var(--pf-c-check__input--MarginTop); +} + +label { + font-size: var(--pf-c-check__label--FontSize); + font-weight: var(--pf-c-check__label--FontWeight); + line-height: var(--pf-c-check__label--LineHeight); + color: var(--pf-c-check__label--Color); +} + +label.disabled { + color: var(--pf-c-check__label--disabled--Color); +} + + +#required { + margin-left: var(--pf-c-check__label-required--MarginLeft); + font-size: var(--pf-c-check__label-required--FontSize); + color: var(--pf-c-check__label-required--Color); +} diff --git a/elements/pf-checkbox/pf-checkbox.ts b/elements/pf-checkbox/pf-checkbox.ts new file mode 100644 index 0000000000..9006528889 --- /dev/null +++ b/elements/pf-checkbox/pf-checkbox.ts @@ -0,0 +1,218 @@ +import { LitElement, html } from 'lit'; +import { customElement } from 'lit/decorators/custom-element.js'; + +import { property } from 'lit/decorators/property.js'; +import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js'; +import { SlotController } from '@patternfly/pfe-core/controllers/slot-controller.js'; +import { classMap } from 'lit/directives/class-map.js'; + +import styles from './pf-checkbox.css'; + +export class RestoreEvent extends Event { + constructor(public element: PfCheckbox) { + super('restore', { bubbles: true, composed: true }); + } +} + +/** + * A **checkbox** is used to select a single item or multiple items, typically to choose elements to perform an action or to reflect a binary setting. + * + * @slot - Place nested (i.e. 'controlled') patternfly form-control elements here + * @slot description - Description text of the checkbox. + * @slot body - Body text of the checkbox. + * + * @fires {RestoreEvent} restore - when the form state is restored. + * @fires change - An event for when the checkbox selection changes. + * + * @attr name - Form name of the checkbox + * + * @cssprop --pf-c-check__label--Color {@default var(--pf-global--Color--100, #151515)} + * @cssprop --pf-c-check__label--FontWeight {@default var(--pf-global--FontWeight--normal, 400)} + * @cssprop --pf-c-check__label--FontSize {@default var(--pf-global--FontSize--md, 1rem)} + * @cssprop --pf-c-check__label--LineHeight {@default var(--pf-global--LineHeight--sm, 1.3)} + * @cssprop --pf-c-check__input--Height {@default var(--pf-c-check__label--FontSize)} + * @cssprop --pf-c-check__input--MarginTop {@default calc(((var(--pf-c-check__label--FontSize) * var(--pf-c-check__label--LineHeight)) - var(--pf-c-check__input--Height)) / 2)} + * @cssprop --pf-c-check__description--FontSize {@default var(--pf-global--FontSize--sm, 0.875rem)} + * @cssprop --pf-c-check__description--Color {@default var(--pf-global--Color--200, #6a6e73)} + * @cssprop --pf-c-check__body--MarginTop {@default var(--pf-global--spacer--sm, 0.5rem)} + * @cssprop --pf-c-check__label-required--MarginLeft {@default var(--pf-global--spacer--xs, 0.25rem)} + * @cssprop --pf-c-check__label-required--FontSize {@default var(--pf-global--FontSize--sm, 0.875rem)} + * @cssprop --pf-c-check__label-required--Color {@default var(--pf-global--danger-color--100)} + */ +@customElement('pf-checkbox') +export class PfCheckbox extends LitElement { + static readonly styles = [styles]; + + static formAssociated = true; + + static shadowRootOptions: ShadowRootInit = { ...LitElement.shadowRootOptions, delegatesFocus: true }; + + /** + * Label text of the checkbox. + */ + @property() label?: string; + + /** + * Form value of the checkbox + */ + @property() value = ''; + + /** + * Flag to show if the checkbox is indeterminate. + */ + @property({ type: Boolean, reflect: true }) indeterminate = false; + + /** + * Flag to show if the checkbox is checked. + */ + @property({ type: Boolean, reflect: true }) checked = false; + + /** + * Flag to show if the checkbox is disabled. + */ + @property({ type: Boolean, reflect: true }) disabled = false; + + /** + * Flag to show if the checkbox is required. + */ + @property({ type: Boolean, reflect: true }) required = false; + + @queryAssignedElements() private nestedElements?: HTMLElement[]; + + /** + * Flag to show if the checkbox selection is valid or invalid. + */ + get valid() { + return this.#internals.validity.valid; + } + + #internals = this.attachInternals(); + + #slots = new SlotController(this, null, 'description', 'body'); + + #batching = false; + + constructor() { + super(); + this.addEventListener('restore', e => this.#onRestore(e)); + } + + override willUpdate() { + this.#internals.setFormValue(this.checked ? this.value : null); + this.#setDisabledStateOnNestedControls(); + } + + override render() { + const { checked, disabled, indeterminate, required } = this; + const emptyNested = !this.#slots.hasSlotted(SlotController.anonymous as unknown as string); + const emptyDescription = !this.#slots.hasSlotted('description'); + const emptyBody = !this.#slots.hasSlotted('body'); + return html` + + + + + + `; + } + + async formDisabledCallback() { + await this.updateComplete; + this.requestUpdate(); + } + + async formStateRestoreCallback(state: string, mode: string) { + if (mode === 'restore') { + const [maybeControlMode, maybeValue] = state.split('/'); + if (maybeValue ?? maybeControlMode === this.value) { + this.checked = true; + } + this.dispatchEvent(new RestoreEvent(this)); + } + } + + #onChange(event: Event) { + this.checked = (event.target as HTMLInputElement).checked; + this.#toggleAll(this.checked); + } + + async #onRestore(event: Event) { + if (event instanceof RestoreEvent && event.element !== this) { + await this.updateComplete; + this.#onSlottedChange(); + this.#setDisabledStateOnNestedControls(); + } + } + + async #onSlottedChange() { + if (this.#batching) { + return; + } + let checked = false; + let unchecked = false; + for (const el of this.nestedElements ?? []) { + if (el instanceof PfCheckbox) { + checked ||= el.checked; + unchecked ||= !el.checked; + } + } + this.checked = checked && !unchecked; + this.indeterminate = checked && unchecked; + } + + async #setDisabledStateOnNestedControls() { + const controls = this.nestedElements?.filter(x => !(x instanceof PfCheckbox)) ?? []; + if (controls.length) { + await Promise.race([ + new Promise(r => setTimeout(r, 1000)), + Promise.all(controls.map(x => + customElements.whenDefined(x.localName))) + ]); + for (const control of controls) { + if ('disabled' in control) { + control.disabled = !this.checked; + } + } + } + } + + #toggleAll(force: boolean) { + this.#batching = true; + for (const el of this.nestedElements ?? []) { + if (el instanceof PfCheckbox) { + el.checked = force; + } + } + this.#batching = false; + } + + setCustomValidity(message: string) { + this.#internals.setValidity({}, message); + } + + checkValidity() { + return this.#internals.checkValidity(); + } + + reportValidity() { + return this.#internals.reportValidity(); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'pf-checkbox': PfCheckbox; + } +} diff --git a/elements/pf-checkbox/test/pf-checkbox.e2e.ts b/elements/pf-checkbox/test/pf-checkbox.e2e.ts new file mode 100644 index 0000000000..eabfb4ee2a --- /dev/null +++ b/elements/pf-checkbox/test/pf-checkbox.e2e.ts @@ -0,0 +1,12 @@ +import { test } from '@playwright/test'; +import { PfeDemoPage } from '@patternfly/pfe-tools/test/playwright/PfeDemoPage.js'; + +const tagName = 'pf-checkbox'; + +test.describe(tagName, () => { + test('snapshot', async ({ page }) => { + const componentPage = new PfeDemoPage(page, tagName); + await componentPage.navigate(); + await componentPage.snapshot(); + }); +}); diff --git a/elements/pf-checkbox/test/pf-checkbox.spec.ts b/elements/pf-checkbox/test/pf-checkbox.spec.ts new file mode 100644 index 0000000000..76e800339b --- /dev/null +++ b/elements/pf-checkbox/test/pf-checkbox.spec.ts @@ -0,0 +1,21 @@ +import { expect, html } from '@open-wc/testing'; +import { createFixture } from '@patternfly/pfe-tools/test/create-fixture.js'; +import { PfCheckbox } from '@patternfly/elements/pf-checkbox/pf-checkbox.js'; + +describe('', function() { + describe('simply instantiating', function() { + let element: PfCheckbox; + it('imperatively instantiates', function() { + expect(document.createElement('pf-checkbox')).to.be.an.instanceof(PfCheckbox); + }); + + it('should upgrade', async function() { + element = await createFixture(html``); + const klass = customElements.get('pf-checkbox'); + expect(element) + .to.be.an.instanceOf(klass) + .and + .to.be.an.instanceOf(PfCheckbox); + }); + }); +});