From 5b48b8ff69c2d88bac578c89817ea41c72211b99 Mon Sep 17 00:00:00 2001 From: Ariella Gilmore Date: Fri, 8 Dec 2023 11:19:38 -0800 Subject: [PATCH] feat(radio-button): slug (#11208) * feat(radio-button): slug * fix(radion-button): opening slug and cleanup * fix(tile): style token --- .../radio-button/radio-button-group.ts | 39 +++++- .../components/radio-button/radio-button.scss | 28 +++- .../components/radio-button/radio-button.ts | 128 +++++++++++++----- .../src/components/slug/slug-example-story.ts | 97 +++++++++++++ .../src/components/slug/slug-story.scss | 28 ++-- .../src/components/tile/tile.scss | 2 +- 6 files changed, 267 insertions(+), 55 deletions(-) diff --git a/packages/carbon-web-components/src/components/radio-button/radio-button-group.ts b/packages/carbon-web-components/src/components/radio-button/radio-button-group.ts index 30bbd890628..23ad339831e 100644 --- a/packages/carbon-web-components/src/components/radio-button/radio-button-group.ts +++ b/packages/carbon-web-components/src/components/radio-button/radio-button-group.ts @@ -72,6 +72,30 @@ class CDSRadioButtonGroup extends FormMixin(HostListenerMixin(LitElement)) { } } + /** + * Handles `slotchange` event. + */ + protected _handleSlotChange({ target }: Event) { + const hasContent = (target as HTMLSlotElement) + .assignedNodes() + .filter((elem) => + (elem as HTMLElement).matches !== undefined + ? (elem as HTMLElement).matches( + (this.constructor as typeof CDSRadioButtonGroup).slugItem + ) + : false + ); + + this._hasSlug = Boolean(hasContent); + (hasContent[0] as HTMLElement).setAttribute('size', 'mini'); + this.requestUpdate(); + } + + /** + * `true` if there is a slug. + */ + protected _hasSlug = false; + /** * The `value` attribute for the `` for selection. */ @@ -189,6 +213,8 @@ class CDSRadioButtonGroup extends FormMixin(HostListenerMixin(LitElement)) { orientation, legendText, helperText, + _hasSlug: hasSlug, + _handleSlotChange: handleSlotChange, } = this; const showWarning = !readOnly && !invalid && warn; @@ -211,6 +237,7 @@ class CDSRadioButtonGroup extends FormMixin(HostListenerMixin(LitElement)) { [`${prefix}--radio-button-group--readonly`]: readOnly, [`${prefix}--radio-button-group--${orientation}`]: orientation === 'vertical', + [`${prefix}--radio-button-group--slug`]: hasSlug, }); return html`
${legendText - ? html` ${legendText}` + ? html` + ${legendText} + + ` : ``}
@@ -244,6 +274,13 @@ class CDSRadioButtonGroup extends FormMixin(HostListenerMixin(LitElement)) { return `${prefix}-radio-button`; } + /** + * A selector that will return the slug item. + */ + static get slugItem() { + return `${prefix}-slug`; + } + /** * The name of the custom event fired after this radio button group changes its selected item. */ diff --git a/packages/carbon-web-components/src/components/radio-button/radio-button.scss b/packages/carbon-web-components/src/components/radio-button/radio-button.scss index 8ecee2b7f27..be4d0742fc0 100644 --- a/packages/carbon-web-components/src/components/radio-button/radio-button.scss +++ b/packages/carbon-web-components/src/components/radio-button/radio-button.scss @@ -11,6 +11,7 @@ $css--plex: true !default; @use '@carbon/styles/scss/theme' as *; @use '@carbon/styles/scss/spacing' as *; @use '@carbon/styles/scss/utilities' as *; +@use '@carbon/styles/scss/utilities/convert' as *; @use '@carbon/styles/scss/components/form'; @use '@carbon/styles/scss/components/radio-button/radio-button' as *; @@ -58,8 +59,13 @@ $css--plex: true !default; } } +:host(#{$prefix}-radio-button[orientation='vertical']) { + margin-block-end: to-rem(6px); + margin-inline-end: 0; +} + :host(#{$prefix}-radio-button[invalid]) .#{$prefix}--radio-button__appearance { - border-color: $support-error; + border-color: $support-error !important; /* stylelint-disable-line declaration-no-important */ } :host(#{$prefix}-radio-button[data-table]) { @@ -102,3 +108,23 @@ $css--plex: true !default; margin-left: $spacing-03; } } + +:host(#{$prefix}-radio-button[slug]) { + .#{$prefix}--radio-button__label-text { + display: flex; + } +} + +:host(#{$prefix}-radio-button-group) .#{$prefix}--radio-button-group--slug, +:host(#{$prefix}-radio-button[slug]) { + ::slotted(#{$prefix}-slug) { + margin-inline-start: $spacing-03; + } +} + +:host(#{$prefix}-radio-button[slug]) { + ::slotted(#{$prefix}-slug[inline]) { + line-height: inherit; + margin-block-start: to-rem(-1px); + } +} diff --git a/packages/carbon-web-components/src/components/radio-button/radio-button.ts b/packages/carbon-web-components/src/components/radio-button/radio-button.ts index 7845beb5c6d..46300710944 100644 --- a/packages/carbon-web-components/src/components/radio-button/radio-button.ts +++ b/packages/carbon-web-components/src/components/radio-button/radio-button.ts @@ -120,7 +120,7 @@ class CDSRadioButton extends HostListenerMixin(FocusMixin(LitElement)) { /** * The hidden radio button. */ - @query('#input') + @query('input') private _inputNode!: HTMLInputElement; /** @@ -128,25 +128,31 @@ class CDSRadioButton extends HostListenerMixin(FocusMixin(LitElement)) { */ @HostListener('click') // @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to - private _handleClick = () => { - const { disabled, _radioButtonDelegate: radioButtonDelegate } = this; - if (radioButtonDelegate && !disabled && !this.disabledItem) { - this.checked = true; - if (this._manager) { - this._manager.select(radioButtonDelegate, this.readOnly); + private _handleClick = (event) => { + if ( + !(event.target as HTMLElement).matches( + (this.constructor as typeof CDSRadioButton)?.slugItem + ) + ) { + const { disabled, _radioButtonDelegate: radioButtonDelegate } = this; + if (radioButtonDelegate && !disabled && !this.disabledItem) { + this.checked = true; + if (this._manager) { + this._manager.select(radioButtonDelegate, this.readOnly); + } + this.dispatchEvent( + new CustomEvent( + (this.constructor as typeof CDSRadioButton).eventChange, + { + bubbles: true, + composed: true, + detail: { + checked: this.checked, + }, + } + ) + ); } - this.dispatchEvent( - new CustomEvent( - (this.constructor as typeof CDSRadioButton).eventChange, - { - bubbles: true, - composed: true, - detail: { - checked: this.checked, - }, - } - ) - ); } }; @@ -156,26 +162,60 @@ class CDSRadioButton extends HostListenerMixin(FocusMixin(LitElement)) { @HostListener('keydown') // @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to private _handleKeydown = (event: KeyboardEvent) => { - const { orientation, _radioButtonDelegate: radioButtonDelegate } = this; - const manager = this._manager; - if (radioButtonDelegate && manager) { - const navigationDirectionForKey = - orientation === RADIO_BUTTON_ORIENTATION.HORIZONTAL - ? navigationDirectionForKeyHorizontal - : navigationDirectionForKeyVertical; - const navigationDirection = navigationDirectionForKey[event.key]; - if (navigationDirection) { - manager.select( - manager.navigate(radioButtonDelegate, navigationDirection), - this.readOnly - ); - } - if (event.key === ' ' || event.key === 'Enter') { - manager.select(radioButtonDelegate, this.readOnly); + if ( + !(event.target as HTMLElement).matches( + (this.constructor as typeof CDSRadioButton)?.slugItem + ) + ) { + const { orientation, _radioButtonDelegate: radioButtonDelegate } = this; + const manager = this._manager; + if (radioButtonDelegate && manager) { + const navigationDirectionForKey = + orientation === RADIO_BUTTON_ORIENTATION.HORIZONTAL + ? navigationDirectionForKeyHorizontal + : navigationDirectionForKeyVertical; + const navigationDirection = navigationDirectionForKey[event.key]; + if (navigationDirection) { + manager.select( + manager.navigate(radioButtonDelegate, navigationDirection), + this.readOnly + ); + } + if (event.key === ' ' || event.key === 'Enter') { + manager.select(radioButtonDelegate, this.readOnly); + } } } }; + /** + * Handles `slotchange` event. + */ + protected _handleSlotChange({ target }: Event) { + const hasContent = (target as HTMLSlotElement) + .assignedNodes() + .filter((elem) => + (elem as HTMLElement).matches !== undefined + ? (elem as HTMLElement).matches( + (this.constructor as typeof CDSRadioButton).slugItem + ) + : false + ); + + this._hasSlug = Boolean(hasContent); + const type = (hasContent[0] as HTMLElement).getAttribute('kind'); + (hasContent[0] as HTMLElement).setAttribute( + 'size', + type === 'inline' ? 'md' : 'mini' + ); + this.requestUpdate(); + } + + /** + * `true` if there is a slug. + */ + protected _hasSlug = false; + /** * `true` if this radio button should be checked. */ @@ -261,10 +301,12 @@ class CDSRadioButton extends HostListenerMixin(FocusMixin(LitElement)) { updated(changedProperties) { const { + _hasSlug: hasSlug, _inputNode: inputNode, _radioButtonDelegate: radioButtonDelegate, name, } = this; + if (changedProperties.has('checked') || changedProperties.has('name')) { if (this.readOnly) { this.checked = false; @@ -288,6 +330,7 @@ class CDSRadioButton extends HostListenerMixin(FocusMixin(LitElement)) { : '0' ); } + hasSlug ? this.setAttribute('slug', '') : this.removeAttribute('slug'); } render() { @@ -301,11 +344,12 @@ class CDSRadioButton extends HostListenerMixin(FocusMixin(LitElement)) { disabledItem, } = this; const innerLabelClasses = classMap({ + [`${prefix}--radio-button__label-text`]: true, [`${prefix}--visually-hidden`]: hideLabel, }); return html` `; } + /** + * A selector that will return the slug item. + */ + static get slugItem() { + return `${prefix}-slug`; + } + /** * The name of the custom event fired after this radio button changes its checked state. */ diff --git a/packages/carbon-web-components/src/components/slug/slug-example-story.ts b/packages/carbon-web-components/src/components/slug/slug-example-story.ts index cf0c40291fc..f82325b1a10 100644 --- a/packages/carbon-web-components/src/components/slug/slug-example-story.ts +++ b/packages/carbon-web-components/src/components/slug/slug-example-story.ts @@ -12,6 +12,7 @@ import { boolean } from '@storybook/addon-knobs'; import View16 from '@carbon/icons/lib/view/16'; import FolderOpen16 from '@carbon/icons/lib/folder--open/16'; import Folders16 from '@carbon/icons/lib/folders/16'; +import textNullable from '../../../.storybook/knob-text-nullable'; import { prefix } from '../../globals/settings'; import './index'; import '../icon-button/index'; @@ -210,6 +211,102 @@ export const _NumberItem = () => { `; }; +export const _RadioButton = (args) => { + const { disabled, invalid, invalidText, warn, warnText } = + args?.['cds-radio-button'] ?? {}; + + return html` + + + ${content}${actions} + + + + + + + + ${content}${actions} + + + ${content}${actions} + + + + + + + + ${content}${actions} + + + + + ${content}${actions} + + + + + `; +}; + +_RadioButton.parameters = { + knobs: { + [`${prefix}-radio-button`]: () => ({ + disabled: boolean('Disabled (disabled)', false), + invalid: boolean('Invalid (invalid)', false), + invalidText: textNullable( + 'Invalid text (invalid-text)', + 'Error message goes here' + ), + warn: boolean('Warn (warn)', false), + warnText: textNullable( + 'Warn text (warn-text)', + 'Warning message that is really long can wrap to more lines but should not be excessively long.' + ), + }), + }, +}; + export const _Select = () => { return html`