From 693f7afa5440c8ccb8ca59c9915a913d9fb0517f Mon Sep 17 00:00:00 2001 From: Arathy-s <ask6295@gmail.com> Date: Thu, 21 Nov 2024 13:05:05 +0530 Subject: [PATCH 01/14] chore: added radio button component --- elements/package.json | 1 + elements/pf-radio/README.md | 11 ++++++ elements/pf-radio/demo/pf-radio.html | 12 +++++++ elements/pf-radio/docs/pf-radio.md | 17 +++++++++ elements/pf-radio/pf-radio.css | 3 ++ elements/pf-radio/pf-radio.ts | 46 +++++++++++++++++++++++++ elements/pf-radio/test/pf-radio.e2e.ts | 25 ++++++++++++++ elements/pf-radio/test/pf-radio.spec.ts | 21 +++++++++++ 8 files changed, 136 insertions(+) create mode 100644 elements/pf-radio/README.md create mode 100644 elements/pf-radio/demo/pf-radio.html create mode 100644 elements/pf-radio/docs/pf-radio.md create mode 100644 elements/pf-radio/pf-radio.css create mode 100644 elements/pf-radio/pf-radio.ts create mode 100644 elements/pf-radio/test/pf-radio.e2e.ts create mode 100644 elements/pf-radio/test/pf-radio.spec.ts diff --git a/elements/package.json b/elements/package.json index 4b159d1d9c..3d3b40a2fd 100644 --- a/elements/package.json +++ b/elements/package.json @@ -36,6 +36,7 @@ "./pf-jump-links/pf-jump-links-list.js": "./pf-jump-links/pf-jump-links-list.js", "./pf-jump-links/pf-jump-links.js": "./pf-jump-links/pf-jump-links.js", "./pf-label/pf-label.js": "./pf-label/pf-label.js", + "./pf-radio/pf-radio.js": "./pf-radio/pf-radio.js", "./pf-select/pf-select.js": "./pf-select/pf-select.js", "./pf-select/pf-listbox.js": "./pf-select/pf-listbox.js", "./pf-select/pf-option-group.js": "./pf-select/pf-option-group.js", diff --git a/elements/pf-radio/README.md b/elements/pf-radio/README.md new file mode 100644 index 0000000000..42617ae474 --- /dev/null +++ b/elements/pf-radio/README.md @@ -0,0 +1,11 @@ +# Radio +Add a description of the component here. + +## Usage +Describe how best to use this web component along with best practices. + +```html +<pf-radio> + +</pf-radio> +``` diff --git a/elements/pf-radio/demo/pf-radio.html b/elements/pf-radio/demo/pf-radio.html new file mode 100644 index 0000000000..6d16656326 --- /dev/null +++ b/elements/pf-radio/demo/pf-radio.html @@ -0,0 +1,12 @@ +<pf-radio></pf-radio> + +<script type="module"> + import '@patternfly/elements/pf-radio/pf-radio.js'; +</script> + +<style> +pf-radio { + /* insert demo styles */ +} +</style> + diff --git a/elements/pf-radio/docs/pf-radio.md b/elements/pf-radio/docs/pf-radio.md new file mode 100644 index 0000000000..c2bd3a1b21 --- /dev/null +++ b/elements/pf-radio/docs/pf-radio.md @@ -0,0 +1,17 @@ +{% renderOverview %} + <pf-radio></pf-radio> +{% endrenderOverview %} + +{% band header="Usage" %}{% endband %} + +{% renderSlots %}{% endrenderSlots %} + +{% renderAttributes %}{% endrenderAttributes %} + +{% renderMethods %}{% endrenderMethods %} + +{% renderEvents %}{% endrenderEvents %} + +{% renderCssCustomProperties %}{% endrenderCssCustomProperties %} + +{% renderCssParts %}{% endrenderCssParts %} diff --git a/elements/pf-radio/pf-radio.css b/elements/pf-radio/pf-radio.css new file mode 100644 index 0000000000..5d4e87f30f --- /dev/null +++ b/elements/pf-radio/pf-radio.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/elements/pf-radio/pf-radio.ts b/elements/pf-radio/pf-radio.ts new file mode 100644 index 0000000000..2fee69a3b9 --- /dev/null +++ b/elements/pf-radio/pf-radio.ts @@ -0,0 +1,46 @@ +import { LitElement, html, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators/custom-element.js'; + +import styles from './pf-radio.css'; +import { property } from 'lit/decorators/property.js'; + +/** + * Radio + * @slot - Place element content here + */ +@customElement('pf-radio') +export class PfRadio extends LitElement { + static readonly styles: CSSStyleSheet[] = [styles]; + @property() checked = false; + @property({ reflect: true }) name = 'radio-test'; + @property({ reflect: true }) label?: string; + // #input:any + + constructor() { + super(); + } + + connectedCallback(): void { + super.connectedCallback(); + + const root = this.getRootNode(); + if (root instanceof Document || root instanceof ShadowRoot) { + const group = root.querySelectorAll(`pf-radio`); + // console.log("------------- the group is", group); + } + } + + + render(): TemplateResult<1> { + return html` + <label for=input>${this.label}</label> + <input id=input class="pf-radio" .name=${this.name} type="radio" .checked="${this.checked}"> + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'pf-radio': PfRadio; + } +} diff --git a/elements/pf-radio/test/pf-radio.e2e.ts b/elements/pf-radio/test/pf-radio.e2e.ts new file mode 100644 index 0000000000..da6108b886 --- /dev/null +++ b/elements/pf-radio/test/pf-radio.e2e.ts @@ -0,0 +1,25 @@ +import { test } from '@playwright/test'; +import { PfeDemoPage } from '@patternfly/pfe-tools/test/playwright/PfeDemoPage.js'; +import { SSRPage } from '@patternfly/pfe-tools/test/playwright/SSRPage.js'; + +const tagName = 'pf-radio'; + +test.describe(tagName, () => { + test('snapshot', async ({ page }) => { + const componentPage = new PfeDemoPage(page, tagName); + await componentPage.navigate(); + await componentPage.snapshot(); + }); + + test('ssr', async ({ browser }) => { + const fixture = new SSRPage({ + tagName, + browser, + demoDir: new URL('../demo/', import.meta.url), + importSpecifiers: [ + `@patternfly/elements/${tagName}/${tagName}.js`, + ], + }); + await fixture.snapshots(); + }); +}); diff --git a/elements/pf-radio/test/pf-radio.spec.ts b/elements/pf-radio/test/pf-radio.spec.ts new file mode 100644 index 0000000000..a5e7aa9b8b --- /dev/null +++ b/elements/pf-radio/test/pf-radio.spec.ts @@ -0,0 +1,21 @@ +import { expect, html } from '@open-wc/testing'; +import { createFixture } from '@patternfly/pfe-tools/test/create-fixture.js'; +import { PfRadio } from '@patternfly/elements/pf-radio/pf-radio.js'; + +describe('<pf-radio>', function() { + describe('simply instantiating', function() { + let element: PfRadio; + it('imperatively instantiates', function() { + expect(document.createElement('pf-radio')).to.be.an.instanceof(PfRadio); + }); + + it('should upgrade', async function() { + element = await createFixture<PfRadio>(html`<pf-radio></pf-radio>`); + const klass = customElements.get('pf-radio'); + expect(element) + .to.be.an.instanceOf(klass) + .and + .to.be.an.instanceOf(PfRadio); + }); + }); +}); From b5689778960277da4a2fccedd376b142cefee273 Mon Sep 17 00:00:00 2001 From: Arathy-s <ask6295@gmail.com> Date: Fri, 22 Nov 2024 18:13:19 +0530 Subject: [PATCH 02/14] chore: radio button click functionality implemented --- elements/pf-radio/pf-radio.ts | 47 ++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/elements/pf-radio/pf-radio.ts b/elements/pf-radio/pf-radio.ts index 2fee69a3b9..0290b3b8d7 100644 --- a/elements/pf-radio/pf-radio.ts +++ b/elements/pf-radio/pf-radio.ts @@ -11,10 +11,25 @@ import { property } from 'lit/decorators/property.js'; @customElement('pf-radio') export class PfRadio extends LitElement { static readonly styles: CSSStyleSheet[] = [styles]; - @property() checked = false; + static formAssociated = true; + static shadowRootOptions: ShadowRootInit = { + ...LitElement.shadowRootOptions, + delegatesFocus: true, + }; + + @property({ + type: Boolean, + // attribute: 'inline-filter', + converter: { + fromAttribute: value => value === 'true', + }, + reflect: true, + }) + checked = false; + @property({ reflect: true }) name = 'radio-test'; @property({ reflect: true }) label?: string; - // #input:any + @property({ reflect: true }) value = ''; constructor() { super(); @@ -22,19 +37,33 @@ export class PfRadio extends LitElement { connectedCallback(): void { super.connectedCallback(); + } - const root = this.getRootNode(); - if (root instanceof Document || root instanceof ShadowRoot) { - const group = root.querySelectorAll(`pf-radio`); - // console.log("------------- the group is", group); + #onRadioButtonClick(event: Event) { + if (!this.checked) { + const root: Node = this.getRootNode(); + let radioGroup: NodeListOf<PfRadio>; + if (root instanceof Document || root instanceof ShadowRoot) { + radioGroup = root.querySelectorAll('pf-radio'); + radioGroup.forEach(radio => { + const element: HTMLElement = radio as HTMLElement; + element?.removeAttribute('checked'); + }); + this.checked = true; + } } } - render(): TemplateResult<1> { return html` - <label for=input>${this.label}</label> - <input id=input class="pf-radio" .name=${this.name} type="radio" .checked="${this.checked}"> + <label for='input'>${this.label}</label> + <input + @click=${(e: Event) => this.#onRadioButtonClick(e)} + id='input' + .name=${this.name} + type='radio' + .checked='${this.checked}' + /> `; } } From dec283f8ae2f3d4d0ede8c5b1749fb5262c9d579 Mon Sep 17 00:00:00 2001 From: Arathy-s <ask6295@gmail.com> Date: Sun, 24 Nov 2024 00:44:15 +0530 Subject: [PATCH 03/14] chore: keyboard event handling added --- elements/pf-radio/pf-radio.ts | 118 +++++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 3 deletions(-) diff --git a/elements/pf-radio/pf-radio.ts b/elements/pf-radio/pf-radio.ts index 0290b3b8d7..9aefcac40e 100644 --- a/elements/pf-radio/pf-radio.ts +++ b/elements/pf-radio/pf-radio.ts @@ -30,6 +30,7 @@ export class PfRadio extends LitElement { @property({ reflect: true }) name = 'radio-test'; @property({ reflect: true }) label?: string; @property({ reflect: true }) value = ''; + @property({ reflect: true }) id = ''; constructor() { super(); @@ -37,9 +38,11 @@ export class PfRadio extends LitElement { connectedCallback(): void { super.connectedCallback(); + this.addEventListener('keydown', this.#onKeydown); + document.addEventListener('keydown', this.#onKeyPress); } - #onRadioButtonClick(event: Event) { + #onRadioButtonClick() { if (!this.checked) { const root: Node = this.getRootNode(); let radioGroup: NodeListOf<PfRadio>; @@ -54,12 +57,121 @@ export class PfRadio extends LitElement { } } + #onKeyPress = (event: KeyboardEvent) => { + const root: Node = this.getRootNode(); + let radioGroup: NodeListOf<PfRadio>; + if (root instanceof Document || root instanceof ShadowRoot) { + radioGroup = root.querySelectorAll('pf-radio'); + if (!event.shiftKey && event.key === 'Tab') { + radioGroup.forEach((radio, index) => { + const input = radio.shadowRoot?.querySelector('input') as HTMLInputElement; + // input.tabIndex = -1; + // if(radio.id === this.shadowRoot?.activeElement?.id){ + // //input.tabIndex = -1; + // //event.preventDefault(); + // //root.focusOut() + // }else{ + // //input.tabIndex = 0; + // } + if (radio.checked === true) { + input.tabIndex = 0; + } else if (index === 0) { + input.tabIndex = 0; + } else { + input.tabIndex = -1; + } + }); + } + + if (event.shiftKey && event.key === 'Tab') { + radioGroup.forEach((radio, index) => { + const input = radio.shadowRoot?.querySelector('input') as HTMLInputElement; + // input.tabIndex = 0; + // input.tabIndex = 0; + // if(radio.id === this.shadowRoot?.activeElement?.id){ + // input.tabIndex = 0; + // //event.preventDefault(); + // //root.focusOut() + // }else{ + // //input.tabIndex = 0; + // } + if (radio.checked === true) { + input.tabIndex = 0; + input.focus(); + } else if (index === (radioGroup.length - 1)) { + input.tabIndex = 0; + } else { + input.tabIndex = -1; + } + }); + } + } + }; + + #onKeydown = (event: KeyboardEvent) => { + if (event.key === 'ArrowDown' + || event.key === 'ArrowRight' + || event.key === 'ArrowUp' + || event.key === 'ArrowLeft') { + const root: Node = this.getRootNode(); + let radioGroup: NodeListOf<PfRadio>; + if (root instanceof Document || root instanceof ShadowRoot) { + radioGroup = root.querySelectorAll('pf-radio'); + radioGroup.forEach((radio, index) => { + const element: HTMLElement = radio as HTMLElement; + element?.removeAttribute('checked'); + this.checked = false; + if (radioGroup[index] === event.target ) { + if (event.key === 'ArrowDown' || event.key === 'ArrowRight') { + if ((radioGroup.length - 1) === index) { + radioGroup[0].focus(); + radioGroup[0].checked = true; + } else { + radioGroup[index + 1].focus(); + radioGroup[index + 1].checked = true; + } + } else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') { + if (index === 0) { + radioGroup[radioGroup.length - 1].focus(); + radioGroup[radioGroup.length - 1].checked = true; + } else { + radioGroup[index - 1].focus(); + radioGroup[index - 1].checked = true; + } + } + } + }); + } + } + }; + + + // #onKeydown1 = (event: KeyboardEvent) => { + // switch (event.key) { + // case "ArrowDown": + // //this.#onArrowDown(event); + // // Do something for "down arrow" key press. + // break; + // case "ArrowUp": + // // Do something for "up arrow" key press. + // break; + // case "ArrowLeft": + // // Do something for "left arrow" key press. + // break; + // case "ArrowRight": + // // Do something for "right arrow" key press. + // break; + // default: + // return; // Quit when this doesn't handle the key event. + // } + // }; + render(): TemplateResult<1> { return html` <label for='input'>${this.label}</label> <input - @click=${(e: Event) => this.#onRadioButtonClick(e)} - id='input' + @click=${this.#onRadioButtonClick} + id=${this.id} .name=${this.name} type='radio' .checked='${this.checked}' From 457cb367ed8fa0fee8d82abdea6d39268b58503a Mon Sep 17 00:00:00 2001 From: Arathy-s <ask6295@gmail.com> Date: Tue, 26 Nov 2024 14:58:13 +0530 Subject: [PATCH 04/14] chore: tab navigation implemented --- elements/pf-radio/pf-radio.ts | 90 ++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 44 deletions(-) diff --git a/elements/pf-radio/pf-radio.ts b/elements/pf-radio/pf-radio.ts index 9aefcac40e..9f287ba3aa 100644 --- a/elements/pf-radio/pf-radio.ts +++ b/elements/pf-radio/pf-radio.ts @@ -19,18 +19,28 @@ export class PfRadio extends LitElement { @property({ type: Boolean, - // attribute: 'inline-filter', + attribute: 'checked', converter: { fromAttribute: value => value === 'true', }, reflect: true, - }) - checked = false; + }) checked = false; + + @property({ + type: Boolean, + attribute: 'disabled', + converter: { + fromAttribute: value => value === 'true', + }, + reflect: true, + }) disabled = false; + + @property({ attribute: 'name', reflect: true }) name = 'radio-test'; + @property({ attribute: 'label', reflect: true }) label?: string; + @property({ attribute: 'value', reflect: true }) value = ''; + @property({ attribute: 'id', reflect: true }) id = ''; + @property({ attribute: 'tabindex', reflect: true }) tabIndex = -1; - @property({ reflect: true }) name = 'radio-test'; - @property({ reflect: true }) label?: string; - @property({ reflect: true }) value = ''; - @property({ reflect: true }) id = ''; constructor() { super(); @@ -51,8 +61,10 @@ export class PfRadio extends LitElement { radioGroup.forEach(radio => { const element: HTMLElement = radio as HTMLElement; element?.removeAttribute('checked'); + element.tabIndex = -1; }); this.checked = true; + this.tabIndex = 0; } } } @@ -60,50 +72,38 @@ export class PfRadio extends LitElement { #onKeyPress = (event: KeyboardEvent) => { const root: Node = this.getRootNode(); let radioGroup: NodeListOf<PfRadio>; + let isRadioChecked = false; if (root instanceof Document || root instanceof ShadowRoot) { radioGroup = root.querySelectorAll('pf-radio'); - if (!event.shiftKey && event.key === 'Tab') { + if (event.key === 'Tab') { radioGroup.forEach((radio, index) => { - const input = radio.shadowRoot?.querySelector('input') as HTMLInputElement; - // input.tabIndex = -1; - // if(radio.id === this.shadowRoot?.activeElement?.id){ - // //input.tabIndex = -1; - // //event.preventDefault(); - // //root.focusOut() - // }else{ - // //input.tabIndex = 0; - // } + radio.tabIndex = -1; if (radio.checked === true) { - input.tabIndex = 0; - } else if (index === 0) { - input.tabIndex = 0; - } else { - input.tabIndex = -1; + radio.tabIndex = 0; + isRadioChecked = true; } }); - } - - if (event.shiftKey && event.key === 'Tab') { - radioGroup.forEach((radio, index) => { - const input = radio.shadowRoot?.querySelector('input') as HTMLInputElement; - // input.tabIndex = 0; - // input.tabIndex = 0; - // if(radio.id === this.shadowRoot?.activeElement?.id){ - // input.tabIndex = 0; - // //event.preventDefault(); - // //root.focusOut() - // }else{ - // //input.tabIndex = 0; - // } - if (radio.checked === true) { - input.tabIndex = 0; - input.focus(); - } else if (index === (radioGroup.length - 1)) { - input.tabIndex = 0; - } else { - input.tabIndex = -1; + if (!isRadioChecked) { + if (event.key === 'Tab') { + radioGroup.forEach((radio, index) => { + radio.tabIndex = -1; + if ( event.shiftKey ) { + if (index === (radioGroup.length - 1)) { + radio.tabIndex = 0; + } else { + radio.tabIndex = -1; + } + } + if (!event.shiftKey) { + if (index === 0) { + radio.tabIndex = 0; + } else { + radio.tabIndex = -1; + } + } + }); } - }); + } } } }; @@ -121,6 +121,7 @@ export class PfRadio extends LitElement { const element: HTMLElement = radio as HTMLElement; element?.removeAttribute('checked'); this.checked = false; + radio.tabIndex = 0; if (radioGroup[index] === event.target ) { if (event.key === 'ArrowDown' || event.key === 'ArrowRight') { if ((radioGroup.length - 1) === index) { @@ -174,6 +175,7 @@ export class PfRadio extends LitElement { id=${this.id} .name=${this.name} type='radio' + tabindex=${this.tabIndex} .checked='${this.checked}' /> `; From 76b553dc8dff871e8ef3a6b643e9d38457b7cfee Mon Sep 17 00:00:00 2001 From: Arathy-s <ask6295@gmail.com> Date: Tue, 26 Nov 2024 14:59:37 +0530 Subject: [PATCH 05/14] chore: associated form label added --- elements/pf-radio/pf-radio.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elements/pf-radio/pf-radio.ts b/elements/pf-radio/pf-radio.ts index 9f287ba3aa..0d4463ec0e 100644 --- a/elements/pf-radio/pf-radio.ts +++ b/elements/pf-radio/pf-radio.ts @@ -169,7 +169,7 @@ export class PfRadio extends LitElement { render(): TemplateResult<1> { return html` - <label for='input'>${this.label}</label> + <label for=${this.id}>${this.label}</label> <input @click=${this.#onRadioButtonClick} id=${this.id} From f0d269d93a0144213a03173ba5df9d924f8f7a58 Mon Sep 17 00:00:00 2001 From: Arathy-s <ask6295@gmail.com> Date: Tue, 26 Nov 2024 20:57:56 +0530 Subject: [PATCH 06/14] chore: code cleanup --- elements/pf-radio/pf-radio.ts | 118 +++++++++++----------------------- 1 file changed, 38 insertions(+), 80 deletions(-) diff --git a/elements/pf-radio/pf-radio.ts b/elements/pf-radio/pf-radio.ts index 0d4463ec0e..71beb68bed 100644 --- a/elements/pf-radio/pf-radio.ts +++ b/elements/pf-radio/pf-radio.ts @@ -24,7 +24,8 @@ export class PfRadio extends LitElement { fromAttribute: value => value === 'true', }, reflect: true, - }) checked = false; + }) + checked = false; @property({ type: Boolean, @@ -33,15 +34,15 @@ export class PfRadio extends LitElement { fromAttribute: value => value === 'true', }, reflect: true, - }) disabled = false; + }) + disabled = false; - @property({ attribute: 'name', reflect: true }) name = 'radio-test'; + @property({ attribute: 'name', reflect: true }) name = ''; @property({ attribute: 'label', reflect: true }) label?: string; @property({ attribute: 'value', reflect: true }) value = ''; @property({ attribute: 'id', reflect: true }) id = ''; @property({ attribute: 'tabindex', reflect: true }) tabIndex = -1; - constructor() { super(); } @@ -58,7 +59,7 @@ export class PfRadio extends LitElement { let radioGroup: NodeListOf<PfRadio>; if (root instanceof Document || root instanceof ShadowRoot) { radioGroup = root.querySelectorAll('pf-radio'); - radioGroup.forEach(radio => { + radioGroup.forEach((radio: PfRadio) => { const element: HTMLElement = radio as HTMLElement; element?.removeAttribute('checked'); element.tabIndex = -1; @@ -69,104 +70,61 @@ export class PfRadio extends LitElement { } } + // Function to handle tab key navigation #onKeyPress = (event: KeyboardEvent) => { const root: Node = this.getRootNode(); - let radioGroup: NodeListOf<PfRadio>; - let isRadioChecked = false; if (root instanceof Document || root instanceof ShadowRoot) { - radioGroup = root.querySelectorAll('pf-radio'); + const radioGroup: NodeListOf<PfRadio> = root.querySelectorAll('pf-radio'); + const isRadioChecked: boolean = Array.from(radioGroup).some( + (radio: PfRadio) => radio.checked + ); if (event.key === 'Tab') { - radioGroup.forEach((radio, index) => { - radio.tabIndex = -1; - if (radio.checked === true) { - radio.tabIndex = 0; - isRadioChecked = true; - } + radioGroup.forEach((radio: PfRadio) => { + radio.tabIndex = radio.checked ? 0 : -1; }); if (!isRadioChecked) { - if (event.key === 'Tab') { - radioGroup.forEach((radio, index) => { - radio.tabIndex = -1; - if ( event.shiftKey ) { - if (index === (radioGroup.length - 1)) { - radio.tabIndex = 0; - } else { - radio.tabIndex = -1; - } + radioGroup.forEach((radio: PfRadio, index: number) => { + radio.tabIndex = -1; + if (event.shiftKey) { + if (index === radioGroup.length - 1) { + radio.tabIndex = 0; } - if (!event.shiftKey) { - if (index === 0) { - radio.tabIndex = 0; - } else { - radio.tabIndex = -1; - } - } - }); - } + } else if (index === 0) { + radio.tabIndex = 0; + } + }); } } } }; + // Function to handle keyboard navigation #onKeydown = (event: KeyboardEvent) => { - if (event.key === 'ArrowDown' - || event.key === 'ArrowRight' - || event.key === 'ArrowUp' - || event.key === 'ArrowLeft') { + const arrowKeys: string[] = ['ArrowDown', 'ArrowRight', 'ArrowUp', 'ArrowLeft']; + if (arrowKeys.includes(event.key)) { const root: Node = this.getRootNode(); - let radioGroup: NodeListOf<PfRadio>; if (root instanceof Document || root instanceof ShadowRoot) { - radioGroup = root.querySelectorAll('pf-radio'); - radioGroup.forEach((radio, index) => { - const element: HTMLElement = radio as HTMLElement; - element?.removeAttribute('checked'); + const radioGroup: NodeListOf<PfRadio> = root.querySelectorAll('pf-radio'); + radioGroup.forEach((radio: PfRadio, index: number) => { this.checked = false; - radio.tabIndex = 0; - if (radioGroup[index] === event.target ) { - if (event.key === 'ArrowDown' || event.key === 'ArrowRight') { - if ((radioGroup.length - 1) === index) { - radioGroup[0].focus(); - radioGroup[0].checked = true; - } else { - radioGroup[index + 1].focus(); - radioGroup[index + 1].checked = true; - } - } else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') { - if (index === 0) { - radioGroup[radioGroup.length - 1].focus(); - radioGroup[radioGroup.length - 1].checked = true; - } else { - radioGroup[index - 1].focus(); - radioGroup[index - 1].checked = true; - } + this.tabIndex = 0; + + if (radio === event.target) { + const isArrowDownOrRight: boolean = ['ArrowDown', 'ArrowRight'].includes(event.key); + const isArrowUpOrLeft: boolean = ['ArrowUp', 'ArrowLeft'].includes(event.key); + const direction: 1 | 0 | -1 = isArrowDownOrRight ? 1 : isArrowUpOrLeft ? -1 : 0; + if (direction === 0) { + return; } + const nextIndex: number = (index + direction + radioGroup.length) % radioGroup.length; + radioGroup[nextIndex].focus(); + radioGroup[nextIndex].checked = true; } }); } } }; - - // #onKeydown1 = (event: KeyboardEvent) => { - // switch (event.key) { - // case "ArrowDown": - // //this.#onArrowDown(event); - // // Do something for "down arrow" key press. - // break; - // case "ArrowUp": - // // Do something for "up arrow" key press. - // break; - // case "ArrowLeft": - // // Do something for "left arrow" key press. - // break; - // case "ArrowRight": - // // Do something for "right arrow" key press. - // break; - // default: - // return; // Quit when this doesn't handle the key event. - // } - // }; - render(): TemplateResult<1> { return html` <label for=${this.id}>${this.label}</label> From 23b7d0b5bf9c6dc0b06435f6b83af59004673dab Mon Sep 17 00:00:00 2001 From: Arathy-s <ask6295@gmail.com> Date: Wed, 27 Nov 2024 12:27:35 +0530 Subject: [PATCH 07/14] chore: demo.html updated --- elements/pf-radio/demo/pf-radio.html | 30 +++++++++++++++++++++++++--- elements/pf-radio/pf-radio.ts | 5 ++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/elements/pf-radio/demo/pf-radio.html b/elements/pf-radio/demo/pf-radio.html index 6d16656326..5bab10e169 100644 --- a/elements/pf-radio/demo/pf-radio.html +++ b/elements/pf-radio/demo/pf-radio.html @@ -1,12 +1,36 @@ -<pf-radio></pf-radio> +<section class='container'> + <p> Select a title </p> + <div class='radio-group'> + <pf-radio id="title-mr" label="Mr" name="title" checked=${false}></pf-radio> + <pf-radio id="title-miss" label="Miss" name="title" checked=${false}></pf-radio> + <pf-radio id="title-mrs" label="Mrs" name="title" checked=${false}></pf-radio> + <pf-radio id="title-ms" label="Ms" name="title" checked=${false}></pf-radio> + <pf-radio id="title-dr" label="Dr" name="title" checked=${false}></pf-radio> + <pf-radio id="title-other" label="Other" name="title" checked=${false}></pf-radio> + </div> + <pf-button> Submit</pf-button> +</section> <script type="module"> import '@patternfly/elements/pf-radio/pf-radio.js'; + import '@patternfly/elements/pf-button/pf-button.js'; </script> <style> -pf-radio { - /* insert demo styles */ +.container{ + padding: 3rem; +} +.container p { + font-size: 1.5rem; + margin-block-end: 0.5rem; +} +.radio-group{ + display: flex; + justify-content: flex-start; + padding-bottom: 1rem; +} +.radio-group pf-radio{ + padding-right: 1rem; } </style> diff --git a/elements/pf-radio/pf-radio.ts b/elements/pf-radio/pf-radio.ts index 71beb68bed..ca585437e8 100644 --- a/elements/pf-radio/pf-radio.ts +++ b/elements/pf-radio/pf-radio.ts @@ -1,6 +1,5 @@ import { LitElement, html, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; - import styles from './pf-radio.css'; import { property } from 'lit/decorators/property.js'; @@ -127,15 +126,15 @@ export class PfRadio extends LitElement { render(): TemplateResult<1> { return html` - <label for=${this.id}>${this.label}</label> <input @click=${this.#onRadioButtonClick} id=${this.id} .name=${this.name} type='radio' tabindex=${this.tabIndex} - .checked='${this.checked}' + .checked=${this.checked} /> + <label for=${this.id}>${this.label}</label> `; } } From d14f49c86bbd81ef992ecbdc7d3c4c6ac0a3b09c Mon Sep 17 00:00:00 2001 From: Arathy-s <ask6295@gmail.com> Date: Wed, 27 Nov 2024 13:00:47 +0530 Subject: [PATCH 08/14] chore: radio check event added --- elements/pf-radio/demo/pf-radio.html | 12 ++++++------ elements/pf-radio/pf-radio.ts | 11 ++++++++++- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/elements/pf-radio/demo/pf-radio.html b/elements/pf-radio/demo/pf-radio.html index 5bab10e169..758dc6b5b2 100644 --- a/elements/pf-radio/demo/pf-radio.html +++ b/elements/pf-radio/demo/pf-radio.html @@ -1,12 +1,12 @@ <section class='container'> <p> Select a title </p> <div class='radio-group'> - <pf-radio id="title-mr" label="Mr" name="title" checked=${false}></pf-radio> - <pf-radio id="title-miss" label="Miss" name="title" checked=${false}></pf-radio> - <pf-radio id="title-mrs" label="Mrs" name="title" checked=${false}></pf-radio> - <pf-radio id="title-ms" label="Ms" name="title" checked=${false}></pf-radio> - <pf-radio id="title-dr" label="Dr" name="title" checked=${false}></pf-radio> - <pf-radio id="title-other" label="Other" name="title" checked=${false}></pf-radio> + <pf-radio id="title-mr" value='mr' label="Mr" name="title" checked=${false}></pf-radio> + <pf-radio id="title-miss" value='miss' label="Miss" name="title" checked=${false}></pf-radio> + <pf-radio id="title-mrs" value='mrs' label="Mrs" name="title" checked=${false}></pf-radio> + <pf-radio id="title-ms" value='ms' label="Ms" name="title" checked=${false}></pf-radio> + <pf-radio id="title-dr" value='dr' label="Dr" name="title" checked=${false}></pf-radio> + <pf-radio id="title-other" value='other' label="Other" name="title" checked=${false}></pf-radio> </div> <pf-button> Submit</pf-button> </section> diff --git a/elements/pf-radio/pf-radio.ts b/elements/pf-radio/pf-radio.ts index ca585437e8..7947de93f2 100644 --- a/elements/pf-radio/pf-radio.ts +++ b/elements/pf-radio/pf-radio.ts @@ -3,6 +3,12 @@ import { customElement } from 'lit/decorators/custom-element.js'; import styles from './pf-radio.css'; import { property } from 'lit/decorators/property.js'; +export class PfRadioChangeEvent extends Event { + constructor(public event: Event, public value: string) { + super('change', { bubbles: true }); + } +} + /** * Radio * @slot - Place element content here @@ -52,7 +58,7 @@ export class PfRadio extends LitElement { document.addEventListener('keydown', this.#onKeyPress); } - #onRadioButtonClick() { + #onRadioButtonClick(event: Event) { if (!this.checked) { const root: Node = this.getRootNode(); let radioGroup: NodeListOf<PfRadio>; @@ -65,6 +71,7 @@ export class PfRadio extends LitElement { }); this.checked = true; this.tabIndex = 0; + this.dispatchEvent(new PfRadioChangeEvent(event, this.value)); } } } @@ -118,6 +125,7 @@ export class PfRadio extends LitElement { const nextIndex: number = (index + direction + radioGroup.length) % radioGroup.length; radioGroup[nextIndex].focus(); radioGroup[nextIndex].checked = true; + this.dispatchEvent(new PfRadioChangeEvent(event, radioGroup[nextIndex].value)); } }); } @@ -131,6 +139,7 @@ export class PfRadio extends LitElement { id=${this.id} .name=${this.name} type='radio' + value=${this.value} tabindex=${this.tabIndex} .checked=${this.checked} /> From 95be15a5afed6310269039a9132a9f28f4bf8716 Mon Sep 17 00:00:00 2001 From: Benny Powers <web@bennypowers.com> Date: Wed, 27 Nov 2024 16:18:51 +0200 Subject: [PATCH 09/14] docs(radio): clean up demo --- elements/pf-radio/demo/pf-radio.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/elements/pf-radio/demo/pf-radio.html b/elements/pf-radio/demo/pf-radio.html index 758dc6b5b2..a37f63f17f 100644 --- a/elements/pf-radio/demo/pf-radio.html +++ b/elements/pf-radio/demo/pf-radio.html @@ -1,12 +1,12 @@ -<section class='container'> +<section class="container"> <p> Select a title </p> - <div class='radio-group'> - <pf-radio id="title-mr" value='mr' label="Mr" name="title" checked=${false}></pf-radio> - <pf-radio id="title-miss" value='miss' label="Miss" name="title" checked=${false}></pf-radio> - <pf-radio id="title-mrs" value='mrs' label="Mrs" name="title" checked=${false}></pf-radio> - <pf-radio id="title-ms" value='ms' label="Ms" name="title" checked=${false}></pf-radio> - <pf-radio id="title-dr" value='dr' label="Dr" name="title" checked=${false}></pf-radio> - <pf-radio id="title-other" value='other' label="Other" name="title" checked=${false}></pf-radio> + <div class="radio-group"> + <pf-radio id="title-mr" value="mr" label="Mr" name="title"></pf-radio> + <pf-radio id="title-miss" value="miss" label="Miss" name="title"></pf-radio> + <pf-radio id="title-mrs" value="mrs" label="Mrs" name="title"></pf-radio> + <pf-radio id="title-ms" value="ms" label="Ms" name="title"></pf-radio> + <pf-radio id="title-dr" value="dr" label="Dr" name="title"></pf-radio> + <pf-radio id="title-other" value="other" label="Other" name="title"></pf-radio> </div> <pf-button> Submit</pf-button> </section> From e79a3f7a3a8b7bac2b5a37c1f31a095f010c76cd Mon Sep 17 00:00:00 2001 From: Benny Powers <web@bennypowers.com> Date: Wed, 27 Nov 2024 16:19:02 +0200 Subject: [PATCH 10/14] docs(radio): multiple groups --- elements/pf-radio/demo/multiple-groups.html | 42 +++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 elements/pf-radio/demo/multiple-groups.html diff --git a/elements/pf-radio/demo/multiple-groups.html b/elements/pf-radio/demo/multiple-groups.html new file mode 100644 index 0000000000..c86f235b84 --- /dev/null +++ b/elements/pf-radio/demo/multiple-groups.html @@ -0,0 +1,42 @@ +<section class="container"> + <div class="radio-group"> + <p>Salutation</p> + <pf-radio value="mr" name="title" label="Mr"></pf-radio> + <pf-radio value="miss" name="title" label="Miss"></pf-radio> + <pf-radio value="mrs" name="title" label="Mrs"></pf-radio> + <pf-radio value="ms" name="title" label="Ms"></pf-radio> + <pf-radio value="dr" name="title" label="Dr"></pf-radio> + <pf-radio value="other" name="title" label="Other"></pf-radio> + </div> + <div class="radio-group"> + <p>Score</p> + <pf-radio value="a" name="score" label="A"></pf-radio> + <pf-radio value="b" name="score" label="B"></pf-radio> + <pf-radio value="c" name="score" label="C"></pf-radio> + </div> + <pf-button> Submit</pf-button> +</section> + +<script type="module"> + import '@patternfly/elements/pf-radio/pf-radio.js'; + import '@patternfly/elements/pf-button/pf-button.js'; +</script> + +<style> +.container{ + padding: 3rem; +} +.container p { + font-size: 1.5rem; + margin-block-end: 0.5rem; +} +.radio-group{ + display: flex; + justify-content: flex-start; + padding-bottom: 1rem; +} +.radio-group pf-radio{ + padding-right: 1rem; +} +</style> + From 82275a1738f1e52afdf037b5f07703d6365ff5ac Mon Sep 17 00:00:00 2001 From: Benny Powers <web@bennypowers.com> Date: Wed, 27 Nov 2024 16:19:39 +0200 Subject: [PATCH 11/14] style(radio): refactor tabindex code --- elements/pf-radio/pf-radio.ts | 143 +++++++++++++++++++--------------- 1 file changed, 82 insertions(+), 61 deletions(-) diff --git a/elements/pf-radio/pf-radio.ts b/elements/pf-radio/pf-radio.ts index 7947de93f2..ba4bd43ac3 100644 --- a/elements/pf-radio/pf-radio.ts +++ b/elements/pf-radio/pf-radio.ts @@ -1,7 +1,10 @@ import { LitElement, html, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; -import styles from './pf-radio.css'; import { property } from 'lit/decorators/property.js'; +import { observes } from '@patternfly/pfe-core/decorators/observes.js'; +import { state } from 'lit/decorators/state.js'; + +import styles from './pf-radio.css'; export class PfRadioChangeEvent extends Event { constructor(public event: Event, public value: string) { @@ -16,46 +19,89 @@ export class PfRadioChangeEvent extends Event { @customElement('pf-radio') export class PfRadio extends LitElement { static readonly styles: CSSStyleSheet[] = [styles]; + static formAssociated = true; + static shadowRootOptions: ShadowRootInit = { ...LitElement.shadowRootOptions, delegatesFocus: true, }; - @property({ - type: Boolean, - attribute: 'checked', - converter: { - fromAttribute: value => value === 'true', - }, - reflect: true, - }) + @property({ type: Boolean, reflect: true }) checked = false; - @property({ - type: Boolean, - attribute: 'disabled', - converter: { - fromAttribute: value => value === 'true', - }, - reflect: true, - }) + @property({ type: Boolean, reflect: true }) disabled = false; - @property({ attribute: 'name', reflect: true }) name = ''; - @property({ attribute: 'label', reflect: true }) label?: string; - @property({ attribute: 'value', reflect: true }) value = ''; - @property({ attribute: 'id', reflect: true }) id = ''; - @property({ attribute: 'tabindex', reflect: true }) tabIndex = -1; + @property({ reflect: true }) name = ''; + + @property({ reflect: true }) label?: string; + + @property({ reflect: true }) value = ''; + + @state() private focusable = false; - constructor() { - super(); + /** Radio groups: instances.get(groupName).forEach(pfRadio => { ... }) */ + private static instances = new Map<string, Set<PfRadio>>(); + + private static selected = new Map<string, PfRadio>; + + static { + globalThis.addEventListener('keydown', e => { + switch (e.key) { + case 'Tab': + this.instances.forEach((radioSet, groupName) => { + const selected = this.selected.get(groupName); + [...radioSet].forEach((radio, i, radios) => { + // the radio group has a selected element + // it should be the only focusable member of the group + if (selected) { + radio.focusable = radio === selected; + // when Shift-tabbing into a group, only the last member should be selected + } else if (e.shiftKey) { + radio.focusable = radio === radios.at(-1); + // otherwise, the first member must be focusable + } else { + radio.focusable = i === 0; + } + }); + }); + break; + } + }); } connectedCallback(): void { super.connectedCallback(); this.addEventListener('keydown', this.#onKeydown); - document.addEventListener('keydown', this.#onKeyPress); + } + + @observes('checked') + protected checkedChanged(): void { + if (this.checked) { + PfRadio.selected.set(this.name, this); + } + } + + @observes('name') + protected nameChanged(oldName: string): void { + // reset the map of groupname to selected radio button + if (PfRadio.selected.get(oldName) === this) { + PfRadio.selected.delete(oldName); + PfRadio.selected.set(this.name, this); + } + if (typeof oldName === 'string') { + PfRadio.instances.get(oldName)?.delete(this); + } + if (!PfRadio.instances.has(this.name)) { + PfRadio.instances.set(this.name, new Set()); + } + PfRadio.instances.get(this.name)?.add(this); + } + + disconnectedCallback(): void { + PfRadio.instances.get(this.name)?.delete(this); + super.disconnectedCallback(); } #onRadioButtonClick(event: Event) { @@ -66,44 +112,17 @@ export class PfRadio extends LitElement { radioGroup = root.querySelectorAll('pf-radio'); radioGroup.forEach((radio: PfRadio) => { const element: HTMLElement = radio as HTMLElement; + // avoid removeAttribute: set checked property instead + // even better: listen for `change` on the shadow input, + // and recalculate state from there. element?.removeAttribute('checked'); - element.tabIndex = -1; }); this.checked = true; - this.tabIndex = 0; this.dispatchEvent(new PfRadioChangeEvent(event, this.value)); } } } - // Function to handle tab key navigation - #onKeyPress = (event: KeyboardEvent) => { - const root: Node = this.getRootNode(); - if (root instanceof Document || root instanceof ShadowRoot) { - const radioGroup: NodeListOf<PfRadio> = root.querySelectorAll('pf-radio'); - const isRadioChecked: boolean = Array.from(radioGroup).some( - (radio: PfRadio) => radio.checked - ); - if (event.key === 'Tab') { - radioGroup.forEach((radio: PfRadio) => { - radio.tabIndex = radio.checked ? 0 : -1; - }); - if (!isRadioChecked) { - radioGroup.forEach((radio: PfRadio, index: number) => { - radio.tabIndex = -1; - if (event.shiftKey) { - if (index === radioGroup.length - 1) { - radio.tabIndex = 0; - } - } else if (index === 0) { - radio.tabIndex = 0; - } - }); - } - } - } - }; - // Function to handle keyboard navigation #onKeydown = (event: KeyboardEvent) => { const arrowKeys: string[] = ['ArrowDown', 'ArrowRight', 'ArrowUp', 'ArrowLeft']; @@ -113,7 +132,6 @@ export class PfRadio extends LitElement { const radioGroup: NodeListOf<PfRadio> = root.querySelectorAll('pf-radio'); radioGroup.forEach((radio: PfRadio, index: number) => { this.checked = false; - this.tabIndex = 0; if (radio === event.target) { const isArrowDownOrRight: boolean = ['ArrowDown', 'ArrowRight'].includes(event.key); @@ -125,6 +143,9 @@ export class PfRadio extends LitElement { const nextIndex: number = (index + direction + radioGroup.length) % radioGroup.length; radioGroup[nextIndex].focus(); radioGroup[nextIndex].checked = true; + // TODO: move this to an @observes + // consider the api of this event. + // do we add the group to it? do we fire from every element on every change? this.dispatchEvent(new PfRadioChangeEvent(event, radioGroup[nextIndex].value)); } }); @@ -135,15 +156,15 @@ export class PfRadio extends LitElement { render(): TemplateResult<1> { return html` <input + id="radio" + type="radio" @click=${this.#onRadioButtonClick} - id=${this.id} .name=${this.name} - type='radio' value=${this.value} - tabindex=${this.tabIndex} + tabindex=${this.focusable ? 0 : -1} .checked=${this.checked} - /> - <label for=${this.id}>${this.label}</label> + > + <label for="radio">${this.label}</label> `; } } From 1dc97121194c84d0874da6ad0d26beb9bbefb55b Mon Sep 17 00:00:00 2001 From: Arathy-s <ask6295@gmail.com> Date: Wed, 4 Dec 2024 19:10:56 +0530 Subject: [PATCH 12/14] chore: implemented pf-radio for multiple groups and inside a shadowroot scenarios --- elements/pf-radio/demo/multiple-groups.html | 96 +++++++++- elements/pf-radio/pf-radio.ts | 199 ++++++++++++-------- 2 files changed, 210 insertions(+), 85 deletions(-) diff --git a/elements/pf-radio/demo/multiple-groups.html b/elements/pf-radio/demo/multiple-groups.html index c86f235b84..6cbde72e0e 100644 --- a/elements/pf-radio/demo/multiple-groups.html +++ b/elements/pf-radio/demo/multiple-groups.html @@ -1,25 +1,67 @@ <section class="container"> + <p>Basic <code><pf-radio></code> group</p> <div class="radio-group"> - <p>Salutation</p> + <p>Salutation: </p> <pf-radio value="mr" name="title" label="Mr"></pf-radio> <pf-radio value="miss" name="title" label="Miss"></pf-radio> <pf-radio value="mrs" name="title" label="Mrs"></pf-radio> <pf-radio value="ms" name="title" label="Ms"></pf-radio> - <pf-radio value="dr" name="title" label="Dr"></pf-radio> - <pf-radio value="other" name="title" label="Other"></pf-radio> </div> - <div class="radio-group"> - <p>Score</p> - <pf-radio value="a" name="score" label="A"></pf-radio> - <pf-radio value="b" name="score" label="B"></pf-radio> - <pf-radio value="c" name="score" label="C"></pf-radio> + + <p><code><pf-radio></code> group with different name inside same parent</p> + <div class="radio-group-container"> + <div class="radio-group"> + <p>Salutation: </p> + <pf-radio value="mr" name="title" label="Mr"></pf-radio> + <pf-radio value="miss" name="title" label="Miss"></pf-radio> + <pf-radio value="mrs" name="title" label="Mrs"></pf-radio> + <pf-radio value="ms" name="title" label="Ms"></pf-radio> + <pf-radio value="dr" name="title" label="Dr"></pf-radio> + <pf-radio value="other" name="title" label="Other"></pf-radio> + + <div class='spacing'></div> + + <p>Score: </p> + <pf-radio value="a" name="score" label="A"></pf-radio> + <pf-radio value="b" name="score" label="B"></pf-radio> + <pf-radio value="c" name="score" label="C"></pf-radio> + </div> + </div> + + <p><code><pf-radio></code> group with same name inside different parent</p> + <div class="radio-group-container"> + <div class="radio-group"> + <p>Score: </p> + <pf-radio value="a" name="score" label="A"></pf-radio> + <pf-radio value="b" name="score" label="B"></pf-radio> + <pf-radio value="c" name="score" label="C"></pf-radio> + </div> + + <div class="radio-group"> + <p>Score: </p> + <pf-radio value="a" name="score" label="A"></pf-radio> + <pf-radio value="b" name="score" label="B"></pf-radio> + <pf-radio value="c" name="score" label="C"></pf-radio> + </div> </div> + + <p><code><pf-radio></code> group inside pf-card component (component inside shadowroot)</p> + <pf-card> + <div class="radio-group"> + <p>Score: </p> + <pf-radio value="a" name="score" label="A"></pf-radio> + <pf-radio value="b" name="score" label="B"></pf-radio> + <pf-radio value="c" name="score" label="C"></pf-radio> + <pf-radio value="d" name="score" label="D"></pf-radio> + </div> + </pf-card> <pf-button> Submit</pf-button> </section> <script type="module"> import '@patternfly/elements/pf-radio/pf-radio.js'; import '@patternfly/elements/pf-button/pf-button.js'; + import '@patternfly/elements/pf-card/pf-card.js' </script> <style> @@ -27,16 +69,50 @@ padding: 3rem; } .container p { - font-size: 1.5rem; - margin-block-end: 0.5rem; + font-size: 1.3rem; + margin-block-end: 1rem; +} + +.container .radio-group p { + font-size: 1.1rem; + margin-block-end: 0rem; + margin-block-start: 0rem; + color: var(--rh-color-red-60, #a60000); + padding-right: 0.5rem; +} + +.radio-group-container { + display: flex; + justify-content: flex-start; + flex-direction: row; } + .radio-group{ display: flex; justify-content: flex-start; padding-bottom: 1rem; + padding-right: 2rem; + flex-direction: row; + align-items: baseline; } .radio-group pf-radio{ padding-right: 1rem; } +pf-card{ + margin-bottom: 2rem; + width: fit-content; +} + +pf-card .radio-group{ + padding-top: 2rem; + padding-bottom: 0.5rem; +} +.spacing{ + margin-right: 2rem; +} + +code{ + font-size: 1.2rem; +} </style> diff --git a/elements/pf-radio/pf-radio.ts b/elements/pf-radio/pf-radio.ts index ba4bd43ac3..2cca7fa2a8 100644 --- a/elements/pf-radio/pf-radio.ts +++ b/elements/pf-radio/pf-radio.ts @@ -1,7 +1,7 @@ import { LitElement, html, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; -import { observes } from '@patternfly/pfe-core/decorators/observes.js'; +// import { observes } from '@patternfly/pfe-core/decorators/observes.js'; import { state } from 'lit/decorators/state.js'; import styles from './pf-radio.css'; @@ -43,27 +43,34 @@ export class PfRadio extends LitElement { /** Radio groups: instances.get(groupName).forEach(pfRadio => { ... }) */ private static instances = new Map<string, Set<PfRadio>>(); + private static radioInstances = new Map<Node, Map<string, Set<PfRadio>>>(); - private static selected = new Map<string, PfRadio>; + private static selected = new Map<Node, Map<string, PfRadio>>(); static { globalThis.addEventListener('keydown', e => { switch (e.key) { case 'Tab': - this.instances.forEach((radioSet, groupName) => { - const selected = this.selected.get(groupName); - [...radioSet].forEach((radio, i, radios) => { - // the radio group has a selected element - // it should be the only focusable member of the group - if (selected) { - radio.focusable = radio === selected; - // when Shift-tabbing into a group, only the last member should be selected - } else if (e.shiftKey) { - radio.focusable = radio === radios.at(-1); - // otherwise, the first member must be focusable - } else { - radio.focusable = i === 0; - } + this.radioInstances.forEach((radioGroup, parentNode) => { + radioGroup.forEach((radioSet, groupName) => { + const selectedNode = this.selected.get(parentNode); + const selected = selectedNode?.get(groupName); + [...radioSet].forEach((radio, i, radios) => { + // the radio group has a selected element + // it should be the only focusable member of the group + radio.focusable = false; + if (groupName === radio.name) { + if (selected) { + radio.focusable = radio === selected; + // when Shift-tabbing into a group, only the last member should be selected + } else if (e.shiftKey) { + radio.focusable = radio === radios.at(-1); + // otherwise, the first member must be focusable + } else { + radio.focusable = i === 0; + } + } + }); }); }); break; @@ -74,52 +81,86 @@ export class PfRadio extends LitElement { connectedCallback(): void { super.connectedCallback(); this.addEventListener('keydown', this.#onKeydown); - } - @observes('checked') - protected checkedChanged(): void { - if (this.checked) { - PfRadio.selected.set(this.name, this); + // Function to group radios based on parent node and name + const root: Node = this.getRootNode(); + let radioGroup: NodeListOf<PfRadio>; + if (root instanceof Document || root instanceof ShadowRoot) { + radioGroup = root.querySelectorAll('pf-radio'); + // let radioGroupArray: any[] = []; + radioGroup.forEach((radio: PfRadio) => { + if (radio.parentNode === this.parentNode && radio.name === this.name) { + // radioGroupArray.push(radio); + let map = PfRadio.radioInstances.get(this.parentNode as HTMLElement); + if (!map) { + map = new Map<string, Set<PfRadio>>(); + PfRadio.radioInstances.set(this.parentNode as HTMLElement, map); + } + let set = map.get(this.name); + if (!set) { + set = new Set<PfRadio>(); + map.set(this.name, set); + } + set.add(radio); + } + }); } } - @observes('name') - protected nameChanged(oldName: string): void { - // reset the map of groupname to selected radio button - if (PfRadio.selected.get(oldName) === this) { - PfRadio.selected.delete(oldName); - PfRadio.selected.set(this.name, this); - } - if (typeof oldName === 'string') { - PfRadio.instances.get(oldName)?.delete(this); - } - if (!PfRadio.instances.has(this.name)) { - PfRadio.instances.set(this.name, new Set()); - } - PfRadio.instances.get(this.name)?.add(this); - } + // @observes('checked') + // protected checkedChanged(): void { + // if (this.checked) { + // PfRadio.selected.set(this.name, this); + // } + // } + + // @observes('name') + // protected nameChanged(oldName: string): void { + // // reset the map of groupname to selected radio button + // if (PfRadio.selected.get(oldName) === this) { + // PfRadio.selected.delete(oldName); + // PfRadio.selected.set(this.name, this); + // } + // if (typeof oldName === 'string') { + // PfRadio.instances.get(oldName)?.delete(this); + // } + // if (!PfRadio.instances.has(this.name)) { + // PfRadio.instances.set(this.name, new Set()); + // } + // PfRadio.instances.get(this.name)?.add(this); + // } disconnectedCallback(): void { PfRadio.instances.get(this.name)?.delete(this); super.disconnectedCallback(); } - #onRadioButtonClick(event: Event) { + #onChange(event: Event) { if (!this.checked) { - const root: Node = this.getRootNode(); - let radioGroup: NodeListOf<PfRadio>; - if (root instanceof Document || root instanceof ShadowRoot) { - radioGroup = root.querySelectorAll('pf-radio'); - radioGroup.forEach((radio: PfRadio) => { - const element: HTMLElement = radio as HTMLElement; - // avoid removeAttribute: set checked property instead - // even better: listen for `change` on the shadow input, - // and recalculate state from there. - element?.removeAttribute('checked'); - }); - this.checked = true; - this.dispatchEvent(new PfRadioChangeEvent(event, this.value)); - } + PfRadio.radioInstances.forEach((radioGroup, parentNode) => { + if (parentNode === this.parentNode) { + radioGroup.forEach((radioSet, groupName) => { + if (groupName === this.name) { + [...radioSet].forEach(radio => { + radio.checked = false; + }); + this.checked = true; + this.dispatchEvent(new PfRadioChangeEvent(event, this.value)); + this.#updateSelected(this.parentNode as HTMLElement, this, this.name); + } + }); + } + }); + } + } + + #updateSelected(parentNode: ParentNode, radio: PfRadio, name: string) { + if (!PfRadio.selected.has(parentNode)) { + PfRadio.selected.set(parentNode, new Map<string, PfRadio>()); + } + const nodeMap = PfRadio.selected.get(parentNode); + if (nodeMap) { + PfRadio.selected.get(parentNode)?.set(name, radio); } } @@ -127,42 +168,50 @@ export class PfRadio extends LitElement { #onKeydown = (event: KeyboardEvent) => { const arrowKeys: string[] = ['ArrowDown', 'ArrowRight', 'ArrowUp', 'ArrowLeft']; if (arrowKeys.includes(event.key)) { - const root: Node = this.getRootNode(); - if (root instanceof Document || root instanceof ShadowRoot) { - const radioGroup: NodeListOf<PfRadio> = root.querySelectorAll('pf-radio'); - radioGroup.forEach((radio: PfRadio, index: number) => { - this.checked = false; - - if (radio === event.target) { - const isArrowDownOrRight: boolean = ['ArrowDown', 'ArrowRight'].includes(event.key); - const isArrowUpOrLeft: boolean = ['ArrowUp', 'ArrowLeft'].includes(event.key); - const direction: 1 | 0 | -1 = isArrowDownOrRight ? 1 : isArrowUpOrLeft ? -1 : 0; - if (direction === 0) { - return; + PfRadio.radioInstances.forEach((radioGroup, parentNode) => { + if (parentNode === this.parentNode) { + radioGroup.forEach((radioSet: Set<PfRadio>, groupName: string) => { + if (groupName === this.name) { + this.checked = false; + [...radioSet].forEach((radio: PfRadio, index: number, radios: PfRadio[]) => { + if (radio === event.target) { + const isArrowDownOrRight: boolean = + ['ArrowDown', 'ArrowRight'].includes(event.key); + const isArrowUpOrLeft: boolean = ['ArrowUp', 'ArrowLeft'].includes(event.key); + const direction: 1 | 0 | -1 = isArrowDownOrRight ? 1 : isArrowUpOrLeft ? -1 : 0; + if (direction === 0) { + return; + } + const nextIndex: number = (index + direction + radios.length) % radios.length; + radios[nextIndex].focus(); + radios[nextIndex].checked = true; + // TODO: move this to an @observes + // consider the api of this event. + // do we add the group to it? do we fire from every element on every change? + this.dispatchEvent(new PfRadioChangeEvent(event, radios[nextIndex].value)); + this.#updateSelected(this.parentNode as HTMLElement, + radios[nextIndex], radios[nextIndex].name); + } + }); } - const nextIndex: number = (index + direction + radioGroup.length) % radioGroup.length; - radioGroup[nextIndex].focus(); - radioGroup[nextIndex].checked = true; - // TODO: move this to an @observes - // consider the api of this event. - // do we add the group to it? do we fire from every element on every change? - this.dispatchEvent(new PfRadioChangeEvent(event, radioGroup[nextIndex].value)); - } - }); - } + }); + } + }); } }; + + // Add a pf component and check if there is any change with the values. render(): TemplateResult<1> { return html` <input id="radio" type="radio" - @click=${this.#onRadioButtonClick} + @change=${this.#onChange} .name=${this.name} value=${this.value} tabindex=${this.focusable ? 0 : -1} - .checked=${this.checked} + .checked=${this.checked} > <label for="radio">${this.label}</label> `; From 405c52e6216b26c8118559ad411649241209f22a Mon Sep 17 00:00:00 2001 From: Arathy-s <ask6295@gmail.com> Date: Wed, 4 Dec 2024 19:54:41 +0530 Subject: [PATCH 13/14] chore: code cleanup --- elements/pf-radio/pf-radio.ts | 42 ++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/elements/pf-radio/pf-radio.ts b/elements/pf-radio/pf-radio.ts index 2cca7fa2a8..4030937eaa 100644 --- a/elements/pf-radio/pf-radio.ts +++ b/elements/pf-radio/pf-radio.ts @@ -44,18 +44,17 @@ export class PfRadio extends LitElement { /** Radio groups: instances.get(groupName).forEach(pfRadio => { ... }) */ private static instances = new Map<string, Set<PfRadio>>(); private static radioInstances = new Map<Node, Map<string, Set<PfRadio>>>(); - private static selected = new Map<Node, Map<string, PfRadio>>(); static { - globalThis.addEventListener('keydown', e => { + globalThis.addEventListener('keydown', (e: KeyboardEvent) => { switch (e.key) { case 'Tab': this.radioInstances.forEach((radioGroup, parentNode) => { radioGroup.forEach((radioSet, groupName) => { const selectedNode = this.selected.get(parentNode); const selected = selectedNode?.get(groupName); - [...radioSet].forEach((radio, i, radios) => { + [...radioSet].forEach((radio: PfRadio, i: number, radios: PfRadio[]) => { // the radio group has a selected element // it should be the only focusable member of the group radio.focusable = false; @@ -87,21 +86,19 @@ export class PfRadio extends LitElement { let radioGroup: NodeListOf<PfRadio>; if (root instanceof Document || root instanceof ShadowRoot) { radioGroup = root.querySelectorAll('pf-radio'); - // let radioGroupArray: any[] = []; radioGroup.forEach((radio: PfRadio) => { - if (radio.parentNode === this.parentNode && radio.name === this.name) { - // radioGroupArray.push(radio); - let map = PfRadio.radioInstances.get(this.parentNode as HTMLElement); - if (!map) { - map = new Map<string, Set<PfRadio>>(); - PfRadio.radioInstances.set(this.parentNode as HTMLElement, map); + if (radio.parentNode && radio.parentNode === this.parentNode && radio.name === this.name) { + let radioGroupMap = PfRadio.radioInstances.get(radio.parentNode); + if (!radioGroupMap) { + radioGroupMap = new Map<string, Set<PfRadio>>(); + PfRadio.radioInstances.set(radio.parentNode, radioGroupMap); } - let set = map.get(this.name); - if (!set) { - set = new Set<PfRadio>(); - map.set(this.name, set); + let radioSet: Set<PfRadio> | undefined = radioGroupMap.get(this.name); + if (!radioSet) { + radioSet = new Set<PfRadio>(); + radioGroupMap.set(this.name, radioSet); } - set.add(radio); + radioSet.add(radio); } }); } @@ -132,6 +129,12 @@ export class PfRadio extends LitElement { disconnectedCallback(): void { PfRadio.instances.get(this.name)?.delete(this); + if (this.parentNode) { + const parentNode = PfRadio.radioInstances.get(this.parentNode); + if (parentNode) { + PfRadio.radioInstances.delete(this.parentNode); + } + } super.disconnectedCallback(); } @@ -140,13 +143,13 @@ export class PfRadio extends LitElement { PfRadio.radioInstances.forEach((radioGroup, parentNode) => { if (parentNode === this.parentNode) { radioGroup.forEach((radioSet, groupName) => { - if (groupName === this.name) { + if (this.parentNode && groupName === this.name) { [...radioSet].forEach(radio => { radio.checked = false; }); this.checked = true; this.dispatchEvent(new PfRadioChangeEvent(event, this.value)); - this.#updateSelected(this.parentNode as HTMLElement, this, this.name); + this.#updateSelected(this.parentNode, this, this.name); } }); } @@ -174,7 +177,7 @@ export class PfRadio extends LitElement { if (groupName === this.name) { this.checked = false; [...radioSet].forEach((radio: PfRadio, index: number, radios: PfRadio[]) => { - if (radio === event.target) { + if (this.parentNode && radio === event.target) { const isArrowDownOrRight: boolean = ['ArrowDown', 'ArrowRight'].includes(event.key); const isArrowUpOrLeft: boolean = ['ArrowUp', 'ArrowLeft'].includes(event.key); @@ -189,8 +192,7 @@ export class PfRadio extends LitElement { // consider the api of this event. // do we add the group to it? do we fire from every element on every change? this.dispatchEvent(new PfRadioChangeEvent(event, radios[nextIndex].value)); - this.#updateSelected(this.parentNode as HTMLElement, - radios[nextIndex], radios[nextIndex].name); + this.#updateSelected(this.parentNode, radios[nextIndex], radios[nextIndex].name); } }); } From 3ed8f18653877645e8ec7b712a65d489bdc67f38 Mon Sep 17 00:00:00 2001 From: ArathyKumar <ask6295@gmail.com> Date: Mon, 9 Dec 2024 19:57:34 +0530 Subject: [PATCH 14/14] chore: code cleanup --- elements/pf-radio/pf-radio.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elements/pf-radio/pf-radio.ts b/elements/pf-radio/pf-radio.ts index 4030937eaa..96deacbbea 100644 --- a/elements/pf-radio/pf-radio.ts +++ b/elements/pf-radio/pf-radio.ts @@ -144,7 +144,7 @@ export class PfRadio extends LitElement { if (parentNode === this.parentNode) { radioGroup.forEach((radioSet, groupName) => { if (this.parentNode && groupName === this.name) { - [...radioSet].forEach(radio => { + [...radioSet].forEach((radio: PfRadio) => { radio.checked = false; }); this.checked = true;