From 3c4d5031fac64dd09c12dfad983f99998d3564d7 Mon Sep 17 00:00:00 2001 From: Barkley Date: Thu, 11 Apr 2024 10:04:07 +0200 Subject: [PATCH] fix(storybook): use slots instead of options array --- packages/js-example-app/src/index.client.ts | 6 + packages/js-example-app/src/index.server.ts | 19 ++- .../src/components/radio-group/index.css.ts | 6 +- .../components/radio-group/index.stories.ts | 80 ++++----- .../src/components/radio-group/index.test.ts | 47 +++--- .../src/components/radio-group/index.ts | 152 +++++++++++------- .../components/radio-group/renderFunction.ts | 5 +- .../src/components/radio/index.stories.ts | 6 +- .../ui-library/src/components/radio/index.ts | 50 ++++-- packages/ui-library/src/globals/events.ts | 12 ++ packages/ui-library/src/globals/types.ts | 8 +- 11 files changed, 237 insertions(+), 154 deletions(-) diff --git a/packages/js-example-app/src/index.client.ts b/packages/js-example-app/src/index.client.ts index d5ed2b0a4..4a1cf82ef 100644 --- a/packages/js-example-app/src/index.client.ts +++ b/packages/js-example-app/src/index.client.ts @@ -9,6 +9,7 @@ import { BlrTabBar, BlrTextarea, BlrToggleSwitch, + BlrRadioGroup, } from '@boiler/ui-library'; async function hydrate() { @@ -44,6 +45,7 @@ function init() { const blrInputFieldNumber = document.getElementsByTagName('blr-input-field-number')[0] as BlrInputFieldNumber; const blrTextArea = document.getElementsByTagName('blr-textarea')[0] as BlrTextarea; const blrRadio = document.getElementsByTagName('blr-radio')[0] as BlrRadio; + const blrRadioGroup = document.getElementsByTagName('blr-radio-group')[0] as BlrRadioGroup; const blrToggleSwitch = document.getElementsByTagName('blr-label-toggleswitch')[0] as BlrToggleSwitch; const blrTabBar = document.getElementsByTagName('blr-tab-bar')[0] as BlrTabBar; @@ -185,6 +187,10 @@ function init() { addLog('blr-radio changed: ' + e.detail.selectedValue); }); + blrRadioGroup.addEventListener('blrRadioGroupValueChange', (e) => { + addLog('blr-radio value changed blrRadioGroupValueChange: ' + e.detail.selectedValue); + }); + blrToggleSwitch.addEventListener('blrFocus', () => { addLog('blr-toggleswitch focused'); }); diff --git a/packages/js-example-app/src/index.server.ts b/packages/js-example-app/src/index.server.ts index 469e36378..a0a5cdaef 100644 --- a/packages/js-example-app/src/index.server.ts +++ b/packages/js-example-app/src/index.server.ts @@ -176,7 +176,7 @@ export function* renderIndex() { +
+

Radio Group

+ + + + + +
+

Tab Bar

div { flex-direction: column; } - - .horizontal { + + .horizontal > div { flex-direction: row; } `; diff --git a/packages/ui-library/src/components/radio-group/index.stories.ts b/packages/ui-library/src/components/radio-group/index.stories.ts index 6cb596270..a4f6dddf8 100644 --- a/packages/ui-library/src/components/radio-group/index.stories.ts +++ b/packages/ui-library/src/components/radio-group/index.stories.ts @@ -223,7 +223,12 @@ export default { }, }; -export const BlrRadioGroup = (params: BlrRadioGroupType) => BlrRadioGroupRenderFunction(params); +const radioButtonsAsChildren = html` + e.detail.selectedValue}> + e.detail.selectedValue}> + e.detail.selectedValue}> +`; +export const BlrRadioGroup = (params: BlrRadioGroupType) => BlrRadioGroupRenderFunction(params, radioButtonsAsChildren); BlrRadioGroup.storyName = 'Radio Group'; @@ -233,42 +238,12 @@ const defaultParams: BlrRadioGroupType & { } = { theme: 'Light', sizeVariant: 'md', - direction: 'horizontal', + direction: 'vertical', hasLegend: true, legend: 'Legend-text', hasHint: false, groupHintMessage: 'This is a small hint', groupHintMessageIcon: 'blrInfo', - options: [ - { - value: '0', - label: 'Option 1', - checked: false, - errorMessage: 'OMG! An error!', - hintMessage: 'This is a small hint', - }, - { - value: '1', - label: 'Option 2', - checked: false, - errorMessage: 'OMG! An error!', - hintMessage: 'This is a small hint', - }, - { - value: '2', - label: 'Option 3', - checked: true, - errorMessage: 'OMG! An error!', - hintMessage: 'This is a small hint', - }, - { - value: '4', - label: 'Option 4', - checked: false, - errorMessage: 'OMG! An error!', - hintMessage: 'This is a small hint', - }, - ], disabled: false, readonly: false, required: false, @@ -278,7 +253,6 @@ const defaultParams: BlrRadioGroupType & { ariaLabel: 'Radio Group', radioGroupId: 'Radio Group', name: 'Radio Group ', - blrChange: () => action('blrChange'), blrFocus: () => action('blrFocus'), blrBlur: () => action('blrBlur'), }; @@ -322,25 +296,25 @@ SizeVariant.story = { name: ' ' }; // /** // * The Radio Group component can have a horizontal or a vertical direction. // * */ -// export const Direction = () => { -// return html` -// ${sharedStyles} -//
-// ${BlrRadioGroup({ -// ...defaultParams, -// direction: 'vertical', -// legend: 'Vertical', -// })} -//
-//
-// ${BlrRadioGroup({ -// ...defaultParams, -// direction: 'horizontal', -// legend: 'Horizontal', -// })} -//
-// `; -// }; +export const Direction = () => { + return html` + ${sharedStyles} +
+ ${BlrRadioGroup({ + ...defaultParams, + direction: 'vertical', + legend: 'Vertical', + })} +
+
+ ${BlrRadioGroup({ + ...defaultParams, + direction: 'horizontal', + legend: 'Horizontal', + })} +
+ `; +}; /** * ## Content / Settings @@ -456,7 +430,7 @@ export const FormCaptionGroup = () => { groupErrorMessage: "OMG it's an error", hasHint: true, hasError: true, - groupErrorIcon: 'blrErrorFilled', + groupHintMessageIcon: 'blrErrorFilled', })}
`; diff --git a/packages/ui-library/src/components/radio-group/index.test.ts b/packages/ui-library/src/components/radio-group/index.test.ts index 0667dbb5e..5f1cf1677 100644 --- a/packages/ui-library/src/components/radio-group/index.test.ts +++ b/packages/ui-library/src/components/radio-group/index.test.ts @@ -2,9 +2,10 @@ import '@boiler/ui-library'; import { BlrRadioGroupRenderFunction } from './renderFunction.js'; -import { fixture, expect } from '@open-wc/testing'; +import { fixture, expect, aTimeout } from '@open-wc/testing'; import { querySelectorDeep } from 'query-selector-shadow-dom'; import { BlrRadioGroupType } from './index.js'; +import { html } from 'lit-html'; const sampleParams: BlrRadioGroupType = { theme: 'Light', @@ -13,11 +14,6 @@ const sampleParams: BlrRadioGroupType = { name: 'Default Name', required: false, readonly: false, - options: [ - { label: 'Multi-line option 1', value: 'option1', hintMessage: 'Hint 1', errorMessage: 'Error Message 1' }, - { label: 'Option 2', value: 'option2', hintMessage: 'Hint 2', errorMessage: 'Error Message 2' }, - { label: 'Option 3', value: 'option3', hintMessage: 'Hint 3', errorMessage: 'Error Message 3' }, - ], hasHint: true, groupHintMessage: 'This is a sample hint message', groupHintMessageIcon: 'blrInfo', @@ -27,9 +23,16 @@ const sampleParams: BlrRadioGroupType = { direction: 'horizontal', }; +const radioButtonsAsChildren = html` + + + + +`; + describe('blr-radio-group', () => { it('is having a radioGroup containing the right className', async () => { - const element = await fixture(BlrRadioGroupRenderFunction(sampleParams)); + const element = await fixture(BlrRadioGroupRenderFunction(sampleParams, radioButtonsAsChildren)); const radioGroup = querySelectorDeep('input[type="radio"]', element.getRootNode() as HTMLElement); const className = radioGroup?.className; @@ -38,7 +41,7 @@ describe('blr-radio-group', () => { }); it('has a size md by default', async () => { - const element = await fixture(BlrRadioGroupRenderFunction(sampleParams)); + const element = await fixture(BlrRadioGroupRenderFunction(sampleParams, radioButtonsAsChildren)); const radioGroup = querySelectorDeep('.blr-radio-group', element.getRootNode() as HTMLElement); const className = radioGroup?.className; @@ -47,7 +50,9 @@ describe('blr-radio-group', () => { }); it('has a size sm when "size" is set to "sm" ', async () => { - const element = await fixture(BlrRadioGroupRenderFunction({ ...sampleParams, sizeVariant: 'sm' })); + const element = await fixture( + BlrRadioGroupRenderFunction({ ...sampleParams, sizeVariant: 'sm' }, radioButtonsAsChildren) + ); const radioGroup = querySelectorDeep('.blr-radio-group', element.getRootNode() as HTMLElement); const className = radioGroup?.className; @@ -57,24 +62,30 @@ describe('blr-radio-group', () => { it('has a error state if hasError is true', async () => { const element = await fixture( - BlrRadioGroupRenderFunction({ - ...sampleParams, - hasError: true, - }) + BlrRadioGroupRenderFunction( + { + ...sampleParams, + hasError: true, + }, + radioButtonsAsChildren + ) ); const radioGroup = querySelectorDeep('input[type="radio"]', element.getRootNode() as HTMLElement); + await aTimeout(100); const className = radioGroup?.className; - expect(className).to.contain('error'); }); it('does not have a error state if hasError is false', async () => { const element = await fixture( - BlrRadioGroupRenderFunction({ - ...sampleParams, - hasError: false, - }) + BlrRadioGroupRenderFunction( + { + ...sampleParams, + hasError: false, + }, + radioButtonsAsChildren + ) ); const radioGroup = querySelectorDeep('input[type="radio"]', element.getRootNode() as HTMLElement); diff --git a/packages/ui-library/src/components/radio-group/index.ts b/packages/ui-library/src/components/radio-group/index.ts index 15ed1e765..188cd0fc8 100644 --- a/packages/ui-library/src/components/radio-group/index.ts +++ b/packages/ui-library/src/components/radio-group/index.ts @@ -6,29 +6,42 @@ import { SizelessIconType } from '@boiler/icons'; import { ThemeType } from '../../foundation/_tokens-generated/index.themes.js'; import { staticStyles as staticRadioStyles } from '../../foundation/component-tokens/radio.css.js'; import { staticStyles as staticFormStyles } from '../../foundation/semantic-tokens/form.css.js'; -import { InputSizesType, RadioGroupDirection, RadioOption } from '../../globals/types.js'; +import { InputSizesType, RadioGroupDirection } from '../../globals/types.js'; import { BlrFormCaptionGroupRenderFunction } from '../form-caption-group/renderFunction.js'; import { BlrFormCaptionRenderFunction } from '../form-caption/renderFunction.js'; -import { BlrFormLabelInlineRenderFunction } from '../form-label/form-label-inline/renderFunction.js'; import { TAG_NAME } from './renderFunction.js'; +import { TAG_NAME as RADIO_TAG_NAME } from '../radio/renderFunction.js'; import { LitElementCustom, ElementInterface } from '../../utils/lit/element.js'; +import { + BlrBlurEvent, + BlrFocusEvent, + BlrRadioGroupValueChangeEvent, + createBlrRadioGroupValueChangeEvent, +} from '../../globals/events.js'; + +export type BlrRadioGroupEventHandlers = { + blrFocus?: (event: BlrFocusEvent) => void; + blrBlur?: (event: BlrBlurEvent) => void; + blrRadioGroupValueChange?: (event: BlrRadioGroupValueChangeEvent) => void; +}; + +/** + * @fires blrFocus Radio received focus + * @fires blrBlur Radio lost focus + * @fires blrRadioGroupValueChange Radio selected value changed + */ export class BlrRadioGroup extends LitElementCustom { static styles = [staticFormStyles, staticRadioStyles, componentSpecificStaticStyles]; @property() accessor disabled: boolean | undefined; @property() accessor readonly: boolean | undefined; - @property() accessor checked: boolean | undefined; @property() accessor name: string | undefined; @property() accessor sizeVariant: InputSizesType = 'md'; @property() accessor hasLegend: boolean | undefined; @property() accessor required: boolean | undefined; - @property() accessor blrChange: HTMLElement['oninput'] | undefined; - @property() accessor blrBlur: HTMLElement['blur'] | undefined; - @property() accessor blrFocus: HTMLElement['focus'] | undefined; @property() accessor hasError: boolean | undefined; @property() accessor errorIcon: SizelessIconType | undefined; - @property() accessor options!: RadioOption[]; @property() accessor hasHint = true; @property() accessor groupHintMessageIcon: SizelessIconType | undefined; @property() accessor groupErrorMessage: string | undefined; @@ -36,8 +49,79 @@ export class BlrRadioGroup extends LitElementCustom { @property() accessor groupErrorMessageIcon: SizelessIconType | undefined; @property() accessor legend: string | undefined; @property() accessor direction: RadioGroupDirection = 'horizontal'; - @property() accessor theme: ThemeType = 'Light'; + protected _radioElements: Element[] | undefined; + + protected handleRadioClick = (event: Event) => { + this._radioElements?.forEach((item) => { + const label = item.getAttribute('label'); + const radioInputElement = item.shadowRoot?.querySelector('input') as HTMLInputElement; + + if (label === (event?.target as HTMLInputElement).id) { + radioInputElement.checked = true; + } + }); + + event.preventDefault(); + }; + + firstUpdated() { + const slot = this.renderRoot?.querySelector('slot'); + this._radioElements = slot?.assignedElements({ flatten: false }); + + setTimeout(() => { + { + this._radioElements?.forEach((item, index, elements) => { + if (item.localName !== RADIO_TAG_NAME) { + throw new Error('child component of blr-radio-group must be blr-radio'); + } + + const radioElement = item.shadowRoot?.querySelector('input'); + const radioElementWrapper = item.shadowRoot?.querySelector('.blr-radio'); + + if (this.hasError) { + [radioElement, radioElementWrapper].forEach((element) => element.classList.add('error')); + } + + // eslint-disable-next-line + // @ts-expect-error + if (this.sizeVariant !== item.sizeVariant) item.sizeVariant = this.sizeVariant; + + radioElement.addEventListener('click', this.handleRadioClick); + + Object.defineProperty(radioElement, 'checked', { + set: function (value) { + if (value === false) { + return; + } + + // eslint-disable-next-line + // @ts-expect-error + item.setChecked(value); + elements.forEach((item) => { + const inputElement = item.shadowRoot?.querySelector('input') as HTMLInputElement; + if (inputElement.getAttribute('id') !== radioElement.getAttribute('id')) { + // eslint-disable-next-line + // @ts-expect-error + item.setChecked(false); + } + }); + + this.dispatchEvent( + createBlrRadioGroupValueChangeEvent({ + originalEvent: { ...new Event('change', { bubbles: true, cancelable: true }), target: radioElement }, + selectedValue: radioElement.value, + }) + ); + }, + get: function () { + return this.hasAttribute('checked'); + }, + }); + }); + } + }, 0); + } protected render() { if (!this.sizeVariant) { @@ -56,25 +140,15 @@ export class BlrRadioGroup extends LitElementCustom { [this.sizeVariant]: this.sizeVariant, }); - const radioClasses = classMap({ - [this.theme]: this.theme, - [this.sizeVariant]: this.sizeVariant, - error: this.hasError || false, - }); - const classes = classMap({ [this.theme]: this.theme, [this.sizeVariant]: this.sizeVariant, disabled: this.disabled || false, readonly: this.readonly || false, - checked: this.checked || false, error: this.hasError || false, [this.direction]: this.direction, }); - const calculateOptionId = (label: string) => { - return label.replace(/ /g, '_').toLowerCase(); - }; const captionContent = html` ${this.hasHint && (this.groupHintMessage || this.groupHintMessageIcon) ? BlrFormCaptionRenderFunction({ @@ -98,44 +172,12 @@ export class BlrRadioGroup extends LitElementCustom { return html` ${this.hasLegend - ? html`
${this.legend}
` + ? html`
+ ${this.legend} +
` : nothing} -
- ${this.options && - this.options.map((option: RadioOption) => { - const id = calculateOptionId(option.label); - return html` -
- -
- ${option.label - ? html`${BlrFormLabelInlineRenderFunction({ - labelText: option.label, - forValue: id, - labelSize: this.sizeVariant || 'md', - theme: this.theme, - })}` - : nothing} -
-
- `; - })} +
${this.hasHint || this.hasError @@ -151,4 +193,4 @@ if (!customElements.get(TAG_NAME)) { customElements.define(TAG_NAME, BlrRadioGroup); } -export type BlrRadioGroupType = ElementInterface; +export type BlrRadioGroupType = ElementInterface; diff --git a/packages/ui-library/src/components/radio-group/renderFunction.ts b/packages/ui-library/src/components/radio-group/renderFunction.ts index e4ec97ec9..9c97c5fdd 100644 --- a/packages/ui-library/src/components/radio-group/renderFunction.ts +++ b/packages/ui-library/src/components/radio-group/renderFunction.ts @@ -1,7 +1,8 @@ +import { TemplateResult } from 'lit-html'; import { BlrRadioGroupType } from './index.js'; import { genericBlrComponentRenderer } from '../../utils/typesafe-generic-component-renderer.js'; export const TAG_NAME = 'blr-radio-group'; -export const BlrRadioGroupRenderFunction = (params: BlrRadioGroupType) => - genericBlrComponentRenderer(TAG_NAME, { ...params }); +export const BlrRadioGroupRenderFunction = (params: BlrRadioGroupType, children?: TemplateResult<1>) => + genericBlrComponentRenderer(TAG_NAME, { ...params }, children); diff --git a/packages/ui-library/src/components/radio/index.stories.ts b/packages/ui-library/src/components/radio/index.stories.ts index 5b21b0e66..b4b32b431 100644 --- a/packages/ui-library/src/components/radio/index.stories.ts +++ b/packages/ui-library/src/components/radio/index.stories.ts @@ -228,7 +228,7 @@ const args: BlrRadioType & { } = { theme: 'Light', sizeVariant: 'md', - value: '', + value: 'radioValue', checked: false, label: 'Label', hasHint: false, @@ -243,8 +243,8 @@ const args: BlrRadioType & { errorMessageIcon: undefined, radioId: 'radioId', name: 'Radio Button', - blrChange: () => action('blrChange'), - blrFocus: () => action('blrFocus'), + blrSelectedValueChange: () => action('blrSelectedValueChangeEvent'), + blrFocus: () => action('focused'), blrBlur: () => action('blrBlr'), }; diff --git a/packages/ui-library/src/components/radio/index.ts b/packages/ui-library/src/components/radio/index.ts index b7ad7bc00..32e21d21f 100644 --- a/packages/ui-library/src/components/radio/index.ts +++ b/packages/ui-library/src/components/radio/index.ts @@ -11,9 +11,28 @@ import { ThemeType } from '../../foundation/_tokens-generated/index.themes.js'; import { BlrFormCaptionGroupRenderFunction } from '../form-caption-group/renderFunction.js'; import { BlrFormCaptionRenderFunction } from '../form-caption/renderFunction.js'; import { BlrFormLabelInlineRenderFunction } from '../form-label/form-label-inline/renderFunction.js'; -import { createBlrBlurEvent, createBlrFocusEvent, createBlrSelectedValueChangeEvent } from '../../globals/events.js'; +import { + createBlrBlurEvent, + createBlrFocusEvent, + createBlrSelectedValueChangeEvent, + BlrBlurEvent, + BlrFocusEvent, + BlrCheckedChangeEvent, +} from '../../globals/events.js'; import { LitElementCustom, ElementInterface } from '../../utils/lit/element.js'; +export type BlrRadioEventHandlers = { + blrFocus?: (event: BlrFocusEvent) => void; + blrBlur?: (event: BlrBlurEvent) => void; + blrSelectedValueChangeEvent?: (event: BlrCheckedChangeEvent) => void; +}; + +/** + * @fires blrFocus Radio received focus + * @fires blrBlur Radio lost focus + * @fires blrSelectedValueChangeEvent Radio selected value changed + */ + export class BlrRadio extends LitElementCustom { static styles = [staticFormStyles, staticRadioStyles]; @@ -28,16 +47,12 @@ export class BlrRadio extends LitElementCustom { @property() accessor name: string | undefined; @property() accessor sizeVariant: InputSizesType | undefined = 'md'; @property() accessor required: boolean | undefined; - @property() accessor blrChange: HTMLElement['oninput'] | undefined; - @property() accessor blrBlur: HTMLElement['blur'] | undefined; - @property() accessor blrFocus: HTMLElement['focus'] | undefined; @property() accessor hasError: boolean | undefined; @property() accessor errorMessage: string | undefined; @property() accessor errorMessageIcon: SizelessIconType | undefined; @property() accessor hasHint: boolean | undefined; @property() accessor hintMessage: string | undefined; @property() accessor hintMessageIcon: SizelessIconType | undefined; - @property() accessor theme: ThemeType = 'Light'; protected handleFocus = (event: FocusEvent) => { @@ -53,11 +68,17 @@ export class BlrRadio extends LitElementCustom { }; protected handleChange(event: Event) { - this.dispatchEvent( - createBlrSelectedValueChangeEvent({ originalEvent: event, selectedValue: this._radioNode.value }) - ); + if (!this.disabled) { + this.dispatchEvent( + createBlrSelectedValueChangeEvent({ originalEvent: event, selectedValue: this._radioNode.value }) + ); + } } + protected setChecked = (status: boolean) => { + this.checked = status; + }; + protected render() { if (this.sizeVariant) { const classes = classMap({ @@ -69,6 +90,10 @@ export class BlrRadio extends LitElementCustom { error: this.hasError || false, }); + const calculateOptionId = (label: string) => { + return label.replace(/ /g, '_').toLowerCase(); + }; + const captionContent = html` ${this.hasHint && (this.hintMessage || this.hintMessageIcon) ? html` @@ -97,14 +122,15 @@ export class BlrRadio extends LitElementCustom { ` : nothing} `; - + const id = calculateOptionId(this.label); return html`
${BlrFormLabelInlineRenderFunction({ labelText: this.label, - forValue: this.optionId, + forValue: id, labelSize: this.sizeVariant, theme: this.theme, })} @@ -135,4 +161,4 @@ if (!customElements.get(TAG_NAME)) { customElements.define(TAG_NAME, BlrRadio); } -export type BlrRadioType = ElementInterface; +export type BlrRadioType = ElementInterface; diff --git a/packages/ui-library/src/globals/events.ts b/packages/ui-library/src/globals/events.ts index e793fe58e..27d5a56dd 100644 --- a/packages/ui-library/src/globals/events.ts +++ b/packages/ui-library/src/globals/events.ts @@ -79,6 +79,18 @@ export function createBlrSelectedValueChangeEvent( return new CustomEvent(BlrSelectedValueChangeEventName, { bubbles: true, composed: true, detail }); } +export type BlrRadioGroupValueChangeEventDetail = { + originalEvent: Event; + selectedValue: string; +}; +export type BlrRadioGroupValueChangeEvent = CustomEvent; +export const BlrRadioGroupValueChangeEventName = 'blrRadioGroupValueChange'; +export function createBlrRadioGroupValueChangeEvent( + detail: BlrRadioGroupValueChangeEventDetail +): BlrRadioGroupValueChangeEvent { + return new CustomEvent(BlrRadioGroupValueChangeEventName, { bubbles: true, composed: true, detail }); +} + export type BlrTextValueChangeEventDetail = { originalEvent: Event; inputValue: string; diff --git a/packages/ui-library/src/globals/types.ts b/packages/ui-library/src/globals/types.ts index 275632172..ecab43857 100644 --- a/packages/ui-library/src/globals/types.ts +++ b/packages/ui-library/src/globals/types.ts @@ -41,13 +41,7 @@ export type ButtonGroupSizesType = (typeof ButtonGroupSizes)[number]; export type ResizeType = (typeof Resizes)[number]; export type InputSizesType = (typeof InputSizes)[number]; -export type RadioOption = { - label: string; - value: string; - hintMessage?: string; - errorMessage?: string; - checked?: boolean; -}; + export type IconPositionVariant = 'leading' | 'trailing'; export type WarningLimits = 'warningLimitInt' | 'warningLimitPer'; export type DividerVariationTypes = (typeof DividerVariations)[number];