diff --git a/web-components/packages/carbon-web-components/src/components/combo-box/combo-box-item.ts b/web-components/packages/carbon-web-components/src/components/combo-box/combo-box-item.ts index 45c307b28752..8b80f9baa5d7 100644 --- a/web-components/packages/carbon-web-components/src/components/combo-box/combo-box-item.ts +++ b/web-components/packages/carbon-web-components/src/components/combo-box/combo-box-item.ts @@ -9,7 +9,7 @@ import { customElement } from 'lit/decorators.js'; import { prefix } from '../../globals/settings'; -import BXDropdownItem from '../dropdown/dropdown-item'; +import CDSDropdownItem from '../dropdown/dropdown-item'; import styles from './combo-box.scss'; /** @@ -18,8 +18,8 @@ import styles from './combo-box.scss'; * @element cds-combo-box-item */ @customElement(`${prefix}-combo-box-item`) -class BXComboBoxItem extends BXDropdownItem { +class CDSComboBoxItem extends CDSDropdownItem { static styles = styles; } -export default BXComboBoxItem; +export default CDSComboBoxItem; diff --git a/web-components/packages/carbon-web-components/src/components/combo-box/combo-box-story.mdx b/web-components/packages/carbon-web-components/src/components/combo-box/combo-box-story.mdx index f281da46c784..b93b1f7ef32f 100644 --- a/web-components/packages/carbon-web-components/src/components/combo-box/combo-box-story.mdx +++ b/web-components/packages/carbon-web-components/src/components/combo-box/combo-box-story.mdx @@ -9,13 +9,7 @@ import { cdnJs, cdnCss } from '../../globals/internal/storybook-cdn'; [![Edit carbon-web-components](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/carbon-design-system/carbon-for-ibm-dotcom/tree/main/packages/carbon-web-components/examples/codesandbox/basic/components/combo-box) -Combo box realizes a notion of "filterable dropdown". By default, the dropdown -displays a label when closed. When the user hovers over the label area, a text -cursor will appear. The drawer opens on click (anywhere in the field) and the -user can type to filter through the list of options below. Once the user begins -typing, the close (X) icon will appear to the right of the label. This will -clear any user-inputted text. Selecting an item from the dropdown will close the -drawer and the selected option will replace the default label. +A combobox allows the user to make a selection from a predefined list of options and is typically used when there are a large amount of options to choose from. ## Getting started @@ -34,9 +28,9 @@ import '@carbon/web-components/es/components/combo-box/index.js'; ```html Option 1 Option 2 @@ -49,6 +43,36 @@ import '@carbon/web-components/es/components/combo-box/index.js'; ``` +## Disabled + +A disabled combobox is available but should not be used as the sole means of conveying information. +For example, if the user must complete a previous form input before moving on to the combobox, +make sure to make that clear to the user via an error state on the previous form element in addition +to disabling the next element. + +```html + + Option 1 + Option 2 + Option 3 + Option 4 + Option 5 + Option 6 + Option 7 + Option 8 + +``` + +## Labels and Helper Texts +The label is not a replacement for a helper or title text under any circumstances including space restraints. +A label should be used to provide additive information regarding the format of the input. +In all cases a helper text is required in addition to a placeholder. + ## Selection When user attempts to select a combo box item, `cds-combo-box-beingselected` diff --git a/web-components/packages/carbon-web-components/src/components/combo-box/combo-box-story.ts b/web-components/packages/carbon-web-components/src/components/combo-box/combo-box-story.ts index 97908ce36163..ae6a88546adc 100644 --- a/web-components/packages/carbon-web-components/src/components/combo-box/combo-box-story.ts +++ b/web-components/packages/carbon-web-components/src/components/combo-box/combo-box-story.ts @@ -9,138 +9,177 @@ import { html } from 'lit'; import { ifDefined } from 'lit/directives/if-defined.js'; -import { action } from '@storybook/addon-actions'; -import { boolean, select, text } from '@storybook/addon-knobs'; -import { - DROPDOWN_COLOR_SCHEME, - DROPDOWN_SIZE, - DROPDOWN_TYPE, -} from './combo-box'; +import { boolean, select } from '@storybook/addon-knobs'; +import { DROPDOWN_DIRECTION, DROPDOWN_SIZE } from './combo-box'; import './combo-box-item'; import storyDocs from './combo-box-story.mdx'; import { prefix } from '../../globals/settings'; +import textNullable from '../../../.storybook/knob-text-nullable'; -const colorSchemes = { - [`Regular`]: null, - [`Light (${DROPDOWN_COLOR_SCHEME.LIGHT})`]: DROPDOWN_COLOR_SCHEME.LIGHT, -}; +const items = [ + { + value: 'option-0', + text: 'An example option that is really long to show what should be done to handle long text', + }, + { + value: 'option-1', + text: 'Option 1', + }, + { + value: 'option-2', + text: 'Option 2', + }, + { + value: 'option-3', + text: 'Option 3 - a disabled item', + disabled: true, + }, + { + value: 'option-4', + text: 'Option 4', + }, + { + value: 'option-5', + text: 'Option 5', + }, +]; -const types = { - Regular: null, - [`Inline (${DROPDOWN_TYPE.INLINE})`]: DROPDOWN_TYPE.INLINE, +const directionOptions = { + [`Top`]: DROPDOWN_DIRECTION.TOP, + [`Bottom`]: DROPDOWN_DIRECTION.BOTTOM, }; const sizes = { - 'Regular size': null, [`Small size (${DROPDOWN_SIZE.SMALL})`]: DROPDOWN_SIZE.SMALL, - [`Extra large size (${DROPDOWN_SIZE.EXTRA_LARGE})`]: - DROPDOWN_SIZE.EXTRA_LARGE, + 'Regular size': null, + [`Large size (${DROPDOWN_SIZE.LARGE})`]: DROPDOWN_SIZE.LARGE, +}; + +export const Default = () => { + return html` + + ${items.map( + (elem) => html` + ${elem.text} + ` + )} + + `; +}; + +export const WithLayer = () => { + return html` + +
+ + ${items.map( + (elem) => html` + ${elem.text} + ` + )} + +
+
+ `; }; -export const Default = (args) => { +export const Playground = (args) => { const { - open, - colorScheme, disabled, helperText, invalid, - labelText, + titleText, + hideLabel, + direction, + readOnly, + warn, + warnText, size, - triggerContent, + label, type, - validityMessage, + invalidText, value, - disableSelection, - disableToggle, - onBeforeSelect, - onBeforeToggle, - onSelect, - onToggle, } = args?.[`${prefix}-combo-box`] ?? {}; - const handleBeforeSelect = (event: CustomEvent) => { - if (onBeforeSelect) { - onBeforeSelect(event); - } - if (disableSelection) { - event.preventDefault(); - } - }; - const handleBeforeToggle = (event: CustomEvent) => { - if (onBeforeToggle) { - onBeforeToggle(event); - } - if (disableToggle) { - event.preventDefault(); - } - }; return html` - Option 1 - Option 2 - Option 3 - Option 4 - Option 5 - Option 6 - Option 7 - Option 8 + type="${ifDefined(type)}" + value=${ifDefined(value)} + label=${ifDefined(label)} + ?warn=${warn} + warn-text=${warnText}> + ${items.map( + (elem) => html` + ${elem.text} + ` + )} `; }; -Default.decorators = [ - (story) => html`
${story()}
`, -]; - -Default.storyName = 'Default'; +Playground.parameters = { + knobs: { + [`${prefix}-combo-box`]: () => ({ + direction: select( + 'Direction', + directionOptions, + DROPDOWN_DIRECTION.BOTTOM + ), + disabled: boolean('Disabled (disabled)', false), + helperText: textNullable( + 'Helper text (helper-text)', + 'Optional helper text' + ), + hideLabel: boolean('Hide label (hide-label)', false), + invalid: boolean('Invalid (invalid)', false), + invalidText: textNullable( + 'Invalid text (invalid-text)', + 'invalid selection' + ), + readOnly: boolean('Read only (read-only)', false), + titleText: textNullable('Title text (title-text)', 'ComboBox title'), + size: select('Size (size)', sizes, null), + value: textNullable('Selected value (value)', ''), + label: textNullable('Placeholder (label)', ''), + warn: boolean('Warn (warn)', false), + warnText: textNullable( + 'Warn text (warn-text)', + 'please notice the warning' + ), + }), + }, +}; export default { title: 'Components/Combo box', parameters: { ...storyDocs.parameters, - knobs: { - [`${prefix}-combo-box`]: () => ({ - open: boolean('Open (open)', false), - colorScheme: select('Color scheme (color-scheme)', colorSchemes, null), - disabled: boolean('Disabled (disabled)', false), - helperText: text('Helper text (helper-text)', 'Optional helper text'), - invalid: boolean('Show invalid state (invalid)', false), - labelText: text('Label text (label-text)', 'Combo box title'), - size: select('Dropdown size (size)', sizes, null), - triggerContent: text( - 'The placeholder content (trigger-content)', - 'Filter...' - ), - type: select('UI type (type)', types, null), - validityMessage: text('The validity message (validity-message)', ''), - value: text('The value of the selected item (value)', ''), - disableSelection: boolean( - `Disable user-initiated selection change (Call event.preventDefault() in ${prefix}-combo-box-beingselected event)`, - false - ), - disableToggle: boolean( - `Disable user-initiated toggle of open state (Call event.preventDefault() in ${prefix}-combo-box-beingtoggled event)`, - false - ), - onBeforeSelect: action(`${prefix}-combo-box-beingselected`), - onBeforeToggle: action(`${prefix}-combo-box-beingtoggled`), - onSelect: action(`${prefix}-combo-box-selected`), - onToggle: action(`${prefix}-combo-box-toggled`), - }), - }, }, + decorators: [ + (story, { name }) => { + const width = !name.toLowerCase().includes('layer') ? `width:400px` : ``; + return html`
${story()}
`; + }, + ], }; diff --git a/web-components/packages/carbon-web-components/src/components/combo-box/combo-box.scss b/web-components/packages/carbon-web-components/src/components/combo-box/combo-box.scss index d2ef0cc50b80..6a405b934e37 100644 --- a/web-components/packages/carbon-web-components/src/components/combo-box/combo-box.scss +++ b/web-components/packages/carbon-web-components/src/components/combo-box/combo-box.scss @@ -8,13 +8,17 @@ $css--plex: true !default; @use '@carbon/styles/scss/config' as *; +@use '@carbon/styles/scss/spacing' as *; @use '@carbon/styles/scss/theme' as *; @use '@carbon/styles/scss/utilities' as *; @use '@carbon/styles/scss/components/combo-box/combo-box'; @use '@carbon/styles/scss/components/form'; @use '@carbon/styles/scss/components/text-input/text-input'; +@use '../dropdown/dropdown.scss'; :host(#{$prefix}-combo-box) { + @extend :host(#{$prefix}-dropdown); + outline: none; .#{$prefix}--assistive-text { @@ -23,10 +27,6 @@ $css--plex: true !default; top: -100%; } - .#{$prefix}--label[hidden] { - display: none; - } - .#{$prefix}--list-box__field, // To get more specifity than: // https://github.com/carbon-design-system/carbon/blob/v10.16.0/packages/components/src/components/list-box/_list-box.scss#L126 @@ -35,20 +35,28 @@ $css--plex: true !default; } .#{$prefix}--list-box__menu { + outline: none; top: 100%; margin-top: 1px; } -} -:host(#{$prefix}-combo-box[type='inline']) { - @extend .#{$prefix}--list-box__wrapper--inline; + &[isClosable] { + .#{$prefix}--list-box__invalid-icon { + right: rem(66px) !important; + } + } - .#{$prefix}--list-box__field { - padding-left: 0; + &[disabled], + &[read-only] { + .#{$prefix}--list-box__selection { + pointer-events: none; + } } - .#{$prefix}--text-input { - border-bottom: none; + &[read-only] { + .#{$prefix}--list-box__selection svg { + fill: $icon-disabled; + } } } @@ -58,47 +66,48 @@ $css--plex: true !default; display: block; .#{$prefix}--list-box__menu-item__option { - height: 100%; + height: auto; } -} - -:host(#{$prefix}-combo-box-item[size='sm']) { - height: rem(32px); - .#{$prefix}--list-box__menu-item__option { - padding-top: rem(7px); - padding-bottom: rem(7px); + &[disabled] { + .#{$prefix}--list-box__menu-item__option { + color: $text-disabled; + text-decoration: none; + } } -} - -:host(#{$prefix}-combo-box-item[size='xl']) { - height: rem(48px); - .#{$prefix}--list-box__menu-item__option { - padding-top: rem(15px); - padding-bottom: rem(15px); + &[highlighted] { + @extend .#{$prefix}--list-box__menu-item--highlighted; } -} -:host(#{$prefix}-combo-box-item[disabled]) - .#{$prefix}--list-box__menu-item__option { - color: $text-disabled; - text-decoration: none; -} + &[selected] { + @extend .#{$prefix}--list-box__menu-item--active; + @extend .#{$prefix}--list-box__menu-item--highlighted; -:host(#{$prefix}-combo-box-item[highlighted]) { - @extend .#{$prefix}--list-box__menu-item--highlighted; -} + .#{$prefix}--list-box__menu-item__option { + color: $text-primary; + } -:host(#{$prefix}-combo-box-item[selected]) { - @extend .#{$prefix}--list-box__menu-item--active; - @extend .#{$prefix}--list-box__menu-item--highlighted; + .#{$prefix}--list-box__menu-item__selected-icon { + display: block; + } + } - .#{$prefix}--list-box__menu-item__option { - color: $text-primary; + &[size='sm'] { + height: $spacing-07; + + .#{$prefix}--list-box__menu-item__option { + padding-top: rem(7px); + padding-bottom: rem(7px); + } } - .#{$prefix}--list-box__menu-item__selected-icon { - display: block; + &[size='lg'] { + height: $spacing-09; + + .#{$prefix}--list-box__menu-item__option { + padding-top: rem(15px); + padding-bottom: rem(15px); + } } } diff --git a/web-components/packages/carbon-web-components/src/components/combo-box/combo-box.ts b/web-components/packages/carbon-web-components/src/components/combo-box/combo-box.ts index f512fee41943..59e9ca75b87d 100644 --- a/web-components/packages/carbon-web-components/src/components/combo-box/combo-box.ts +++ b/web-components/packages/carbon-web-components/src/components/combo-box/combo-box.ts @@ -7,20 +7,17 @@ * LICENSE file in the root directory of this source tree. */ +import { classMap } from 'lit/directives/class-map.js'; import { TemplateResult, html } from 'lit'; import { property, customElement, query } from 'lit/decorators.js'; import Close16 from '@carbon/icons/lib/close/16'; import { prefix } from '../../globals/settings'; import { findIndex, forEach } from '../../globals/internal/collection-helpers'; -import BXDropdown, { DROPDOWN_KEYBOARD_ACTION } from '../dropdown/dropdown'; -import BXComboBoxItem from './combo-box-item'; +import CDSDropdown, { DROPDOWN_KEYBOARD_ACTION } from '../dropdown/dropdown'; +import CDSComboBoxItem from './combo-box-item'; import styles from './combo-box.scss'; -export { - DROPDOWN_COLOR_SCHEME, - DROPDOWN_SIZE, - DROPDOWN_TYPE, -} from '../dropdown/dropdown'; +export { DROPDOWN_DIRECTION, DROPDOWN_SIZE } from '../dropdown/dropdown'; /** * Combo box. @@ -36,7 +33,7 @@ export { * @fires cds-combo-box-toggled - The custom event fired after the open state of this combo box is toggled upon a user gesture. */ @customElement(`${prefix}-combo-box`) -class BXComboBox extends BXDropdown { +class CDSComboBox extends CDSDropdown { /** * The text content that should be set to the `` for filtering. */ @@ -82,7 +79,7 @@ class BXComboBox extends BXDropdown { * @returns `true` if the given combo box item matches the given query text. */ protected _defaultItemMatches( - item: BXComboBoxItem, + item: CDSComboBoxItem, queryText: string ): boolean { return ( @@ -95,8 +92,14 @@ class BXComboBox extends BXDropdown { * Handles `input` event on the `` for filtering. */ protected _handleInput() { + if (this._filterInputValue.length != 0) { + this.setAttribute('isClosable', ''); + } else { + this.removeAttribute('isClosable'); + } + const items = this.querySelectorAll( - (this.constructor as typeof BXComboBox).selectorItem + (this.constructor as typeof CDSComboBox).selectorItem ); const index = !this._filterInputNode.value ? -1 @@ -122,7 +125,7 @@ class BXComboBox extends BXDropdown { } } } - (item as BXComboBoxItem).highlighted = i === index; + (item as CDSComboBoxItem).highlighted = i === index; }); const { _filterInputNode: filterInput } = this; this._filterInputValue = !filterInput ? '' : filterInput.value; @@ -131,7 +134,8 @@ class BXComboBox extends BXDropdown { } protected _handleClickInner(event: MouseEvent) { - if (this._selectionButtonNode?.contains(event.target as Node)) { + const { target } = event as any; + if (this._selectionButtonNode?.contains(target)) { this._handleUserInitiatedClearInput(); } else { super._handleClickInner(event); @@ -140,7 +144,7 @@ class BXComboBox extends BXDropdown { protected _handleKeypressInner(event: KeyboardEvent) { const { key } = event; - const action = (this.constructor as typeof BXDropdown).getAction(key); + const action = (this.constructor as typeof CDSDropdown).getAction(key); const { TRIGGERING } = DROPDOWN_KEYBOARD_ACTION; if ( this._selectionButtonNode?.contains(event.target as Node) && @@ -159,10 +163,10 @@ class BXComboBox extends BXDropdown { protected _handleUserInitiatedClearInput() { forEach( this.querySelectorAll( - (this.constructor as typeof BXComboBox).selectorItem + (this.constructor as typeof CDSComboBox).selectorItem ), (item) => { - (item as BXComboBoxItem).highlighted = false; + (item as CDSComboBoxItem).highlighted = false; } ); this._filterInputValue = ''; @@ -170,7 +174,7 @@ class BXComboBox extends BXDropdown { this._handleUserInitiatedSelectItem(); } - protected _handleUserInitiatedSelectItem(item?: BXComboBoxItem) { + protected _handleUserInitiatedSelectItem(item?: CDSComboBoxItem) { if (item && !this._selectionShouldChange(item)) { // Escape hatch for `shouldUpdate()` logic that updates `._filterInputValue()` when selection changes, // given we want to update the `` and close the dropdown even if selection doesn't update. @@ -187,14 +191,14 @@ class BXComboBox extends BXDropdown { super._handleUserInitiatedSelectItem(item); } - protected _selectionDidChange(itemToSelect?: BXComboBoxItem) { + protected _selectionDidChange(itemToSelect?: CDSComboBoxItem) { this.value = !itemToSelect ? '' : itemToSelect.value; forEach( this.querySelectorAll( - (this.constructor as typeof BXDropdown).selectorItemSelected + (this.constructor as typeof CDSDropdown).selectorItemSelected ), (item) => { - (item as BXComboBoxItem).selected = false; + (item as CDSComboBoxItem).selected = false; } ); if (itemToSelect) { @@ -204,31 +208,47 @@ class BXComboBox extends BXDropdown { this._handleUserInitiatedToggle(false); } - protected _renderTriggerContent(): TemplateResult { + protected _renderLabel(): TemplateResult { const { disabled, inputLabel, - triggerContent, + label, + readOnly, + value, _filterInputValue: filterInputValue, _handleInput: handleInput, } = this; + + const inputClasses = classMap({ + [`${prefix}--text-input`]: true, + [`${prefix}--text-input--empty`]: !value, + }); + return html` `; } - protected _renderFollowingTriggerContent(): TemplateResult | void { + protected _renderFollowingLabel(): TemplateResult | void { const { clearSelectionLabel, _filterInputValue: filterInputValue } = this; + + if (filterInputValue.length != 0) { + this.setAttribute('isClosable', ''); + } else { + this.removeAttribute('isClosable'); + } + return filterInputValue.length === 0 ? undefined : html` @@ -259,7 +279,7 @@ class BXComboBox extends BXDropdown { * The custom item matching callback. */ @property({ attribute: false }) - itemMatches!: (item: BXComboBoxItem, queryText: string) => boolean; + itemMatches!: (item: CDSComboBoxItem, queryText: string) => boolean; shouldUpdate(changedProperties) { super.shouldUpdate(changedProperties); @@ -334,4 +354,4 @@ class BXComboBox extends BXDropdown { static styles = styles; } -export default BXComboBox; +export default CDSComboBox; diff --git a/web-components/packages/carbon-web-components/src/components/dropdown/defs.ts b/web-components/packages/carbon-web-components/src/components/dropdown/defs.ts index 16df5ee6da18..9d92040c2262 100644 --- a/web-components/packages/carbon-web-components/src/components/dropdown/defs.ts +++ b/web-components/packages/carbon-web-components/src/components/dropdown/defs.ts @@ -1,7 +1,7 @@ /** * @license * - * Copyright IBM Corp. 2020 + * Copyright IBM Corp. 2020, 2023 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. @@ -59,9 +59,9 @@ export enum DROPDOWN_SIZE { SMALL = 'sm', /** - * Extra large size. + * Large size. */ - EXTRA_LARGE = 'xl', + LARGE = 'lg', } /** @@ -78,3 +78,18 @@ export enum DROPDOWN_TYPE { */ INLINE = 'inline', } + +/** + * Dropdown direction. + */ +export enum DROPDOWN_DIRECTION { + /** + * Top. + */ + TOP = 'top', + + /** + * Bottom. + */ + BOTTOM = 'bottom', +} diff --git a/web-components/packages/carbon-web-components/src/components/dropdown/dropdown-item.ts b/web-components/packages/carbon-web-components/src/components/dropdown/dropdown-item.ts index 2f031661bd13..53e5238345ae 100644 --- a/web-components/packages/carbon-web-components/src/components/dropdown/dropdown-item.ts +++ b/web-components/packages/carbon-web-components/src/components/dropdown/dropdown-item.ts @@ -21,7 +21,7 @@ import styles from './dropdown.scss'; * @csspart selected-icon The selected icon. */ @customElement(`${prefix}-dropdown-item`) -class BXDropdownItem extends LitElement { +class CDSDropdownItem extends LitElement { /** * `true` if this dropdown item should be disabled. */ @@ -75,4 +75,4 @@ class BXDropdownItem extends LitElement { static styles = styles; } -export default BXDropdownItem; +export default CDSDropdownItem; diff --git a/web-components/packages/carbon-web-components/src/components/dropdown/dropdown-skeleton.ts b/web-components/packages/carbon-web-components/src/components/dropdown/dropdown-skeleton.ts index 0a629b795eeb..2e50b23be0a9 100644 --- a/web-components/packages/carbon-web-components/src/components/dropdown/dropdown-skeleton.ts +++ b/web-components/packages/carbon-web-components/src/components/dropdown/dropdown-skeleton.ts @@ -16,7 +16,7 @@ import styles from './dropdown.scss'; * Skeleton version of dropdown. */ @customElement(`${prefix}-dropdown-skeleton`) -class BXDropdownSkeleton extends LitElement { +class CDSDropdownSkeleton extends LitElement { render() { return html`
+ Foo + Bar + Baz + +``` + ## Skeleton For the skeleton variation, utilize ``. diff --git a/web-components/packages/carbon-web-components/src/components/dropdown/dropdown-story.ts b/web-components/packages/carbon-web-components/src/components/dropdown/dropdown-story.ts index f785e0b876f6..4029b51a8ea9 100644 --- a/web-components/packages/carbon-web-components/src/components/dropdown/dropdown-story.ts +++ b/web-components/packages/carbon-web-components/src/components/dropdown/dropdown-story.ts @@ -8,30 +8,24 @@ */ import { html } from 'lit'; -import { action } from '@storybook/addon-actions'; import { boolean, select } from '@storybook/addon-knobs'; import { prefix } from '../../globals/settings'; import textNullable from '../../../.storybook/knob-text-nullable'; import { ifDefined } from 'lit/directives/if-defined.js'; -import { - DROPDOWN_COLOR_SCHEME, - DROPDOWN_SIZE, - DROPDOWN_TYPE, -} from './dropdown'; +import { DROPDOWN_DIRECTION, DROPDOWN_SIZE, DROPDOWN_TYPE } from './dropdown'; import './dropdown-item'; import './dropdown-skeleton'; import storyDocs from './dropdown-story.mdx'; -const colorSchemes = { - [`Regular`]: null, - [`Light (${DROPDOWN_COLOR_SCHEME.LIGHT})`]: DROPDOWN_COLOR_SCHEME.LIGHT, +const directionOptions = { + [`Top`]: DROPDOWN_DIRECTION.TOP, + [`Bottom`]: DROPDOWN_DIRECTION.BOTTOM, }; const sizes = { - 'Regular size': null, [`Small size (${DROPDOWN_SIZE.SMALL})`]: DROPDOWN_SIZE.SMALL, - [`Extra large size (${DROPDOWN_SIZE.EXTRA_LARGE})`]: - DROPDOWN_SIZE.EXTRA_LARGE, + 'Regular size': null, + [`Large size (${DROPDOWN_SIZE.LARGE})`]: DROPDOWN_SIZE.LARGE, }; const types = { @@ -39,100 +33,194 @@ const types = { [`Inline (${DROPDOWN_TYPE.INLINE})`]: DROPDOWN_TYPE.INLINE, }; -export const Default = (args) => { +const items = [ + { + value: 'option-0', + text: 'Lorem, ipsum dolor sit amet consectetur adipisicing elit.', + }, + { + value: 'option-1', + text: 'Option 1', + }, + { + value: 'option-2', + text: 'Option 2', + }, + { + value: 'option-3', + text: 'Option 3 - a disabled item', + disabled: true, + }, + { + value: 'option-4', + text: 'Option 4', + }, + { + value: 'option-5', + text: 'Option 5', + }, +]; + +export const Default = () => { + return html` + + ${items.map( + (elem) => html` + ${elem.text} + ` + )} + + `; +}; + +export const Inline = () => { + return html` + + ${items.map( + (elem) => html` + ${elem.text} + ` + )} + + `; +}; + +export const InlineWithLayer = () => { + return html` + +
+ + ${items.map( + (elem) => html` + ${elem.text} + ` + )} + +
+
+ `; +}; + +export const WithLayer = () => { + return html` + +
+ + ${items.map( + (elem) => html` + ${elem.text} + ` + )} + +
+
+ `; +}; + +export const Playground = (args) => { const { open, - colorScheme, + direction, disabled, helperText, - labelText, + hideLabel, + invalid, + invalidText, + titleText, + readOnly, size, type, value, - triggerContent, - disableSelection, - disableToggle, - onBeforeSelect, - onBeforeToggle, - onSelect, - onToggle, + label, + warn, + warnText, } = args?.[`${prefix}-dropdown`] ?? {}; - const handleBeforeSelect = (event: CustomEvent) => { - if (onBeforeSelect) { - onBeforeSelect(event); - } - if (disableSelection) { - event.preventDefault(); - } - }; - const handleBeforeToggle = (event: CustomEvent) => { - if (onBeforeToggle) { - onBeforeToggle(event); - } - if (disableToggle) { - event.preventDefault(); - } - }; + return html` - Option 1 - Option 2 - Option 3 - Option 4 - Option 5 + label=${ifDefined(label)} + ?warn=${warn} + warn-text=${warnText}> + ${items.map( + (elem) => html` + ${elem.text} + ` + )} `; }; -Default.decorators = [ - (story) => html`
${story()}
`, -]; - -Default.storyName = 'Default'; - -Default.parameters = { +Playground.parameters = { knobs: { [`${prefix}-dropdown`]: () => ({ open: boolean('Open (open)', false), - colorScheme: select('Color scheme (color-scheme)', colorSchemes, null), + direction: select('Direction', directionOptions, null), disabled: boolean('Disabled (disabled)', false), helperText: textNullable( 'Helper text (helper-text)', - 'Optional helper text' + 'This is some helper text' ), - labelText: textNullable('Label text (label-text)', 'Dropdown title'), - size: select('Dropdown size (size)', sizes, null), - type: select('Dropdown type (type)', types, null), - value: textNullable('The value of the selected item (value)', ''), - triggerContent: textNullable( - 'The default content of the trigger button (trigger-content)', - 'Select an item' + hideLabel: boolean('Hide label (hide-label)', false), + invalid: boolean('Invalid (invalid)', false), + invalidText: textNullable( + 'Invalid text (invalid-text)', + 'invalid selection' + ), + readOnly: boolean('Read only (read-only)', false), + label: textNullable( + 'The default content of the trigger button (label)', + 'This is an example label' ), - disableSelection: boolean( - `Disable user-initiated selection change (Call event.preventDefault() in ${prefix}-dropdown-beingselected event)`, - false + titleText: textNullable( + 'Title text (title-text)', + 'This is an example title' ), - disableToggle: boolean( - `Disable user-initiated toggle of open state (Call event.preventDefault() in ${prefix}-dropdown-beingtoggled event)`, - false + size: select('Dropdown size (size)', sizes, null), + type: select('Dropdown type (type)', types, null), + value: textNullable('Selected value (value)', ''), + warn: boolean('Warn (warn)', false), + warnText: textNullable( + 'Warn text (warn-text)', + 'please notice the warning' ), - onBeforeSelect: action(`${prefix}-dropdown-beingselected`), - onBeforeToggle: action(`${prefix}-dropdown-beingtoggled`), - onSelect: action(`${prefix}-dropdown-selected`), - onToggle: action(`${prefix}-dropdown-toggled`), }), }, }; @@ -151,4 +239,10 @@ export default { parameters: { ...storyDocs.parameters, }, + decorators: [ + (story, { name }) => { + const width = !name.toLowerCase().includes('layer') ? `width:400px` : ``; + return html`
${story()}
`; + }, + ], }; diff --git a/web-components/packages/carbon-web-components/src/components/dropdown/dropdown.scss b/web-components/packages/carbon-web-components/src/components/dropdown/dropdown.scss index c96b3cc7a7d3..912d30da2332 100644 --- a/web-components/packages/carbon-web-components/src/components/dropdown/dropdown.scss +++ b/web-components/packages/carbon-web-components/src/components/dropdown/dropdown.scss @@ -8,9 +8,11 @@ $css--plex: true !default; @use '@carbon/styles/scss/config' as *; +@use '@carbon/styles/scss/spacing' as *; @use '@carbon/styles/scss/theme' as *; @use '@carbon/styles/scss/utilities' as *; @use '@carbon/styles/scss/components/list-box/list-box' as *; +@use '@carbon/styles/scss/components/dropdown' as *; @use '@carbon/styles/scss/components/form'; @use '@carbon/styles/scss/components/checkbox'; @use '@carbon/styles/scss/components/tag'; @@ -34,11 +36,61 @@ $css--plex: true !default; .#{$prefix}--list-box__menu { top: 100%; margin-top: 1px; + outline: 1px solid $focus; } -} -:host(#{$prefix}-dropdown[type='inline']) { - @extend .#{$prefix}--list-box__wrapper--inline; + &[open] { + .#{$prefix}--list-box__field { + outline: none; + } + } + + &[invalid] { + .#{$prefix}--list-box__field { + outline: none; + } + + .#{$prefix}--form__helper-text { + color: $text-error; + } + } + + &[read-only] { + @extend .#{$prefix}--dropdown--readonly; + + .#{$prefix}--list-box { + background-color: transparent; + border-bottom-color: $border-subtle; + } + } + + &[direction='top'] { + @extend .#{$prefix}--list-box--up; + + .#{$prefix}--list-box__menu { + top: auto; + } + } + + &[type='inline'] { + @extend .#{$prefix}--list-box__wrapper--inline; + + grid-gap: 0 $spacing-06; + + &[warn] { + .#{$prefix}--list-box__field { + padding-left: $spacing-05; + padding-right: calc(#{$spacing-08} + #{$spacing-05}); + } + } + + &[warn], + &[invalid] { + .#{$prefix}--form__helper-text { + grid-column: 2; + } + } + } } :host(#{$prefix}-dropdown-item) { @@ -47,48 +99,49 @@ $css--plex: true !default; display: block; .#{$prefix}--list-box__menu-item__option { - height: 100%; + height: auto; } -} -:host(#{$prefix}-dropdown-item[size='sm']) { - height: rem(32px); - - .#{$prefix}--list-box__menu-item__option { - padding-top: rem(7px); - padding-bottom: rem(7px); + &[highlighted] { + @extend .#{$prefix}--list-box__menu-item--highlighted; } -} - -:host(#{$prefix}-dropdown-item[size='xl']) { - height: rem(48px); - .#{$prefix}--list-box__menu-item__option { - padding-top: rem(15px); - padding-bottom: rem(15px); + &[disabled] { + .#{$prefix}--list-box__menu-item__option { + color: $text-disabled; + text-decoration: none; + } } -} -:host(#{$prefix}-dropdown-item[disabled]) - .#{$prefix}--list-box__menu-item__option { - color: $text-disabled; - text-decoration: none; -} + &[selected] { + @extend .#{$prefix}--list-box__menu-item--active; + @extend .#{$prefix}--list-box__menu-item--highlighted; -:host(#{$prefix}-dropdown-item[highlighted]) { - @extend .#{$prefix}--list-box__menu-item--highlighted; -} + .#{$prefix}--list-box__menu-item__option { + color: $text-primary; + } -:host(#{$prefix}-dropdown-item[selected]) { - @extend .#{$prefix}--list-box__menu-item--active; - @extend .#{$prefix}--list-box__menu-item--highlighted; + .#{$prefix}--list-box__menu-item__selected-icon { + display: block; + } + } - .#{$prefix}--list-box__menu-item__option { - color: $text-primary; + &[size='sm'] { + height: $spacing-07; + + .#{$prefix}--list-box__menu-item__option { + padding-top: rem(7px); + padding-bottom: rem(7px); + } } - .#{$prefix}--list-box__menu-item__selected-icon { - display: block; + &[size='lg'] { + height: $spacing-09; + + .#{$prefix}--list-box__menu-item__option { + padding-top: rem(15px); + padding-bottom: rem(15px); + } } } diff --git a/web-components/packages/carbon-web-components/src/components/dropdown/dropdown.ts b/web-components/packages/carbon-web-components/src/components/dropdown/dropdown.ts index 626151586f83..bd17858e1026 100644 --- a/web-components/packages/carbon-web-components/src/components/dropdown/dropdown.ts +++ b/web-components/packages/carbon-web-components/src/components/dropdown/dropdown.ts @@ -14,6 +14,7 @@ import { property, customElement, query } from 'lit/decorators.js'; import { prefix } from '../../globals/settings'; import ChevronDown16 from '@carbon/icons/lib/chevron--down/16'; import WarningFilled16 from '@carbon/icons/lib/warning--filled/16'; +import WarningAltFilled16 from '@carbon/icons/lib/warning--alt--filled/16'; import FocusMixin from '../../globals/mixins/focus'; import FormMixin from '../../globals/mixins/form'; import HostListenerMixin from '../../globals/mixins/host-listener'; @@ -25,18 +26,18 @@ import { indexOf, } from '../../globals/internal/collection-helpers'; import { - DROPDOWN_COLOR_SCHEME, + DROPDOWN_DIRECTION, DROPDOWN_KEYBOARD_ACTION, DROPDOWN_SIZE, DROPDOWN_TYPE, NAVIGATION_DIRECTION, } from './defs'; -import BXDropdownItem from './dropdown-item'; +import CDSDropdownItem from './dropdown-item'; import styles from './dropdown.scss'; export { - DROPDOWN_COLOR_SCHEME, DROPDOWN_KEYBOARD_ACTION, + DROPDOWN_DIRECTION, DROPDOWN_SIZE, DROPDOWN_TYPE, NAVIGATION_DIRECTION, @@ -61,7 +62,7 @@ export { * @fires cds-dropdown-toggled - The custom event fired after the open state of this dropdown is toggled upon a user gesture. */ @customElement(`${prefix}-dropdown`) -class BXDropdown extends ValidityMixin( +class CDSDropdown extends ValidityMixin( HostListenerMixin(FormMixin(FocusMixin(LitElement))) ) { /** @@ -93,16 +94,16 @@ class BXDropdown extends ValidityMixin( protected _slotHelperTextNode!: HTMLSlotElement; /** - * The `` element for the label text in the shadow DOM. + * The `` element for the title text in the shadow DOM. */ - @query('slot[name="label-text"]') - protected _slotLabelTextNode!: HTMLSlotElement; + @query('slot[name="title-text"]') + protected _slotTitleTextNode!: HTMLSlotElement; /** * @param itemToSelect A dropdown item. Absense of this argument means clearing selection. * @returns `true` if the selection of this dropdown should change if the given item is selected upon user interaction. */ - protected _selectionShouldChange(itemToSelect?: BXDropdownItem) { + protected _selectionShouldChange(itemToSelect?: CDSDropdownItem) { return !itemToSelect || itemToSelect.value !== this.value; } @@ -113,15 +114,15 @@ class BXDropdown extends ValidityMixin( * A dropdown item. * Absense of this argument means clearing selection, which may be handled by a derived class. */ - protected _selectionDidChange(itemToSelect?: BXDropdownItem) { + protected _selectionDidChange(itemToSelect?: CDSDropdownItem) { if (itemToSelect) { this.value = itemToSelect.value; forEach( this.querySelectorAll( - (this.constructor as typeof BXDropdown).selectorItemSelected + (this.constructor as typeof CDSDropdown).selectorItemSelected ), (item) => { - (item as BXDropdownItem).selected = false; + (item as CDSDropdownItem).selected = false; } ); itemToSelect.selected = true; @@ -136,12 +137,16 @@ class BXDropdown extends ValidityMixin( * @param event The event. */ protected _handleClickInner(event: MouseEvent) { + if (this.readOnly) { + return; + } + if (this.shadowRoot!.contains(event.target as Node)) { this._handleUserInitiatedToggle(); } else { const item = (event.target as Element).closest( - (this.constructor as typeof BXDropdown).selectorItem - ) as BXDropdownItem; + (this.constructor as typeof CDSDropdown).selectorItem + ) as CDSDropdownItem; if (this.contains(item)) { this._handleUserInitiatedSelectItem(item); } @@ -153,7 +158,7 @@ class BXDropdown extends ValidityMixin( */ protected _handleKeydownInner(event: KeyboardEvent) { const { key } = event; - const action = (this.constructor as typeof BXDropdown).getAction(key); + const action = (this.constructor as typeof CDSDropdown).getAction(key); if (!this.open) { switch (action) { case DROPDOWN_KEYBOARD_ACTION.NAVIGATING: @@ -183,7 +188,7 @@ class BXDropdown extends ValidityMixin( */ protected _handleKeypressInner(event: KeyboardEvent) { const { key } = event; - const action = (this.constructor as typeof BXDropdown).getAction(key); + const action = (this.constructor as typeof CDSDropdown).getAction(key); if (!this.open) { switch (action) { case DROPDOWN_KEYBOARD_ACTION.TRIGGERING: @@ -196,10 +201,10 @@ class BXDropdown extends ValidityMixin( switch (action) { case DROPDOWN_KEYBOARD_ACTION.TRIGGERING: { - const constructor = this.constructor as typeof BXDropdown; + const constructor = this.constructor as typeof CDSDropdown; const highlightedItem = this.querySelector( constructor.selectorItemHighlighted - ) as BXDropdownItem; + ) as CDSDropdownItem; if (highlightedItem) { this._handleUserInitiatedSelectItem(highlightedItem); } else { @@ -245,7 +250,11 @@ class BXDropdown extends ValidityMixin( * * @param [item] The dropdown item user wants to select. Absense of this argument means clearing selection. */ - protected _handleUserInitiatedSelectItem(item?: BXDropdownItem) { + protected _handleUserInitiatedSelectItem(item?: CDSDropdownItem) { + if (item?.hasAttribute('disabled')) { + return; + } + if (this._selectionShouldChange(item)) { const init = { bubbles: true, @@ -254,7 +263,7 @@ class BXDropdown extends ValidityMixin( item, }, }; - const constructor = this.constructor as typeof BXDropdown; + const constructor = this.constructor as typeof CDSDropdown; const beforeSelectEvent = new CustomEvent(constructor.eventBeforeSelect, { ...init, cancelable: true, @@ -274,7 +283,7 @@ class BXDropdown extends ValidityMixin( */ protected _handleUserInitiatedToggle(force: boolean = !this.open) { const { eventBeforeToggle, eventToggle } = this - .constructor as typeof BXDropdown; + .constructor as typeof CDSDropdown; const { disabled } = this; const init = { @@ -293,13 +302,12 @@ class BXDropdown extends ValidityMixin( } else { const { selectedItemAssistiveText, - triggerContent, + label, _assistiveStatusText: assistiveStatusText, _selectedItemContent: selectedItemContent, } = this; const selectedItemText = - (selectedItemContent && selectedItemContent.textContent) || - triggerContent; + (selectedItemContent && selectedItemContent.textContent) || label; if ( selectedItemText && assistiveStatusText !== selectedItemAssistiveText @@ -308,10 +316,10 @@ class BXDropdown extends ValidityMixin( } forEach( this.querySelectorAll( - (this.constructor as typeof BXDropdown).selectorItemHighlighted + (this.constructor as typeof CDSDropdown).selectorItemHighlighted ), (item) => { - (item as BXDropdownItem).highlighted = false; + (item as CDSDropdownItem).highlighted = false; } ); } @@ -327,10 +335,10 @@ class BXDropdown extends ValidityMixin( protected _clearHighlight() { forEach( this.querySelectorAll( - (this.constructor as typeof BXDropdown).selectorItem + (this.constructor as typeof CDSDropdown).selectorItem ), (item) => { - (item as BXDropdownItem).highlighted = false; + (item as CDSDropdownItem).highlighted = false; } ); } @@ -341,13 +349,17 @@ class BXDropdown extends ValidityMixin( * @param direction `-1` to navigate backward, `1` to navigate forward. */ protected _navigate(direction: number) { - const constructor = this.constructor as typeof BXDropdown; + const constructor = this.constructor as typeof CDSDropdown; const items = this.querySelectorAll(constructor.selectorItem); const highlightedItem = this.querySelector( constructor.selectorItemHighlighted ); const highlightedIndex = indexOf(items, highlightedItem!); let nextIndex = highlightedIndex + direction; + + if (items[nextIndex]?.hasAttribute('disabled')) { + nextIndex += direction; + } if (nextIndex < 0) { nextIndex = items.length - 1; } @@ -355,7 +367,7 @@ class BXDropdown extends ValidityMixin( nextIndex = 0; } forEach(items, (item, i) => { - (item as BXDropdownItem).highlighted = i === nextIndex; + (item as CDSDropdownItem).highlighted = i === nextIndex; }); const nextItem = items[nextIndex]; @@ -375,7 +387,7 @@ class BXDropdown extends ValidityMixin( /** * @returns The content preceding the trigger button. */ - protected _renderPrecedingTriggerContent(): TemplateResult | void { + protected _renderPrecedingLabel(): TemplateResult | void { return undefined; } /* eslint-enable class-methods-use-this */ @@ -383,11 +395,11 @@ class BXDropdown extends ValidityMixin( /** * @returns The main content of the trigger button. */ - protected _renderTriggerContent(): TemplateResult { - const { triggerContent, _selectedItemContent: selectedItemContent } = this; + protected _renderLabel(): TemplateResult { + const { label, _selectedItemContent: selectedItemContent } = this; return html` ${selectedItemContent || triggerContent}${selectedItemContent || label} `; } @@ -396,7 +408,7 @@ class BXDropdown extends ValidityMixin( /** * @returns The content following the trigger button. */ - protected _renderFollowingTriggerContent(): TemplateResult | void { + protected _renderFollowingLabel(): TemplateResult | void { return undefined; } /* eslint-enable class-methods-use-this */ @@ -415,10 +427,17 @@ class BXDropdown extends ValidityMixin( } /** - * The color scheme. + * 'aria-label' of the ListBox component. + * Specify a label to be read by screen readers on the container node */ - @property({ attribute: 'color-scheme', reflect: true }) - colorScheme = DROPDOWN_COLOR_SCHEME.REGULAR; + @property({ type: String, reflect: true, attribute: 'aria-label' }) + ariaLabel = ''; + + /** + * Specify the direction of the dropdown. Can be either top or bottom. + */ + @property({ type: Boolean, reflect: true }) + direction = false; /** * `true` if this dropdown should be disabled. @@ -432,6 +451,12 @@ class BXDropdown extends ValidityMixin( @property({ attribute: 'helper-text' }) helperText = ''; + /** + * Specify whether the title text should be hidden or not + */ + @property({ type: Boolean, reflect: true, attribute: 'hide-label' }) + hideLabel = false; + /** * `true` to show the UI of the invalid state. */ @@ -439,10 +464,16 @@ class BXDropdown extends ValidityMixin( invalid = false; /** - * The label text. + * Message which is displayed if the value is invalid. */ - @property({ attribute: 'label-text' }) - labelText = ''; + @property({ attribute: 'invalid-text' }) + invalidText = ''; + + /** + * Provide the title text that will be read by a screen reader when visiting this control + */ + @property({ attribute: 'title-text' }) + titleText = ''; /** * Name for the dropdown in the `FormData` @@ -456,6 +487,12 @@ class BXDropdown extends ValidityMixin( @property({ type: Boolean, reflect: true }) open = false; + /** + * Whether or not the Dropdown is readonly + */ + @property({ type: Boolean, reflect: true, attribute: 'read-only' }) + readOnly = false; + /** * `true` if the value is required. */ @@ -500,10 +537,10 @@ class BXDropdown extends ValidityMixin( toggleLabelOpen = ''; /** - * The content of the trigger button. + * Generic label that will be used as the textual representation of what this field is for */ - @property({ attribute: 'trigger-content' }) - triggerContent = ''; + @property({ attribute: 'label' }) + label = ''; /** * `true` if this dropdown should use the inline UI variant. @@ -523,23 +560,35 @@ class BXDropdown extends ValidityMixin( @property({ reflect: true }) value = ''; + /** + * Specify whether the control is currently in warning state + */ + @property({ type: Boolean, reflect: true }) + warn = false; + + /** + * Provide the text that is displayed when the control is in warning state + */ + @property({ attribute: 'warn-text' }) + warnText = ''; + shouldUpdate(changedProperties) { - const { selectorItem } = this.constructor as typeof BXDropdown; + const { selectorItem } = this.constructor as typeof CDSDropdown; if (changedProperties.has('size')) { forEach(this.querySelectorAll(selectorItem), (elem) => { - (elem as BXDropdownItem).size = this.size; + (elem as CDSDropdownItem).size = this.size; }); } if (changedProperties.has('value')) { // `` updates selection beforehand // because our rendering logic for `` looks for selected items via `qSA()` forEach(this.querySelectorAll(selectorItem), (elem) => { - (elem as BXDropdownItem).selected = - (elem as BXDropdownItem).value === this.value; + (elem as CDSDropdownItem).selected = + (elem as CDSDropdownItem).value === this.value; }); const item = find( this.querySelectorAll(selectorItem), - (elem) => (elem as BXDropdownItem).value === this.value + (elem) => (elem as CDSDropdownItem).value === this.value ); if (item) { const range = this.ownerDocument!.createRange(); @@ -555,12 +604,12 @@ class BXDropdown extends ValidityMixin( updated(changedProperties) { const { helperText, type } = this; const inline = type === DROPDOWN_TYPE.INLINE; - const { selectorItem } = this.constructor as typeof BXDropdown; - if (changedProperties.has('disabled')) { + const { selectorItem } = this.constructor as typeof CDSDropdown; + if (changedProperties.has('disabled') && this.disabled) { const { disabled } = this; // Propagate `disabled` attribute to descendants until `:host-context()` gets supported in all major browsers forEach(this.querySelectorAll(selectorItem), (elem) => { - (elem as BXDropdownItem).disabled = disabled; + (elem as CDSDropdownItem).disabled = disabled; }); } if ( @@ -578,17 +627,20 @@ class BXDropdown extends ValidityMixin( render() { const { - colorScheme, + ariaLabel, disabled, helperText, + hideLabel, invalid, - labelText, + invalidText, open, toggleLabelClosed, toggleLabelOpen, size, + titleText, type, - validityMessage, + warn, + warnText, _assistiveStatusText: assistiveStatusText, _shouldTriggerBeFocusable: shouldTriggerBeFocusable, _handleClickInner: handleClickInner, @@ -597,27 +649,28 @@ class BXDropdown extends ValidityMixin( _handleSlotchangeHelperText: handleSlotchangeHelperText, _handleSlotchangeLabelText: handleSlotchangeLabelText, _slotHelperTextNode: slotHelperTextNode, - _slotLabelTextNode: slotLabelTextNode, + _slotTitleTextNode: slotTitleTextNode, } = this; const inline = type === DROPDOWN_TYPE.INLINE; const selectedItemsCount = this.querySelectorAll( - (this.constructor as typeof BXDropdown).selectorItemSelected + (this.constructor as typeof CDSDropdown).selectorItemSelected ).length; const classes = classMap({ [`${prefix}--dropdown`]: true, [`${prefix}--list-box`]: true, - [`${prefix}--list-box--${colorScheme}`]: colorScheme, [`${prefix}--list-box--disabled`]: disabled, [`${prefix}--list-box--inline`]: inline, [`${prefix}--list-box--expanded`]: open, [`${prefix}--list-box--${size}`]: size, [`${prefix}--dropdown--invalid`]: invalid, + [`${prefix}--dropdown--warn`]: warn, [`${prefix}--dropdown--inline`]: inline, [`${prefix}--dropdown--selected`]: selectedItemsCount > 0, }); const labelClasses = classMap({ [`${prefix}--label`]: true, [`${prefix}--label--disabled`]: disabled, + [`${prefix}--visually-hidden`]: hideLabel, }); const helperClasses = classMap({ [`${prefix}--form__helper-text`]: true, @@ -632,35 +685,28 @@ class BXDropdown extends ValidityMixin( const hasHelperText = helperText || (slotHelperTextNode && slotHelperTextNode.assignedNodes().length > 0); - const hasLabelText = - labelText || - (slotLabelTextNode && slotLabelTextNode.assignedNodes().length > 0); - const helper = !invalid - ? html` -
- ${helperText} -
- ` - : html` -
- ${validityMessage} -
- `; + const hasTitleText = + titleText || + (slotTitleTextNode && slotTitleTextNode.assignedNodes().length > 0); const validityIcon = !invalid ? undefined : WarningFilled16({ class: `${prefix}--list-box__invalid-icon`, 'aria-label': toggleLabel, }); + const warningIcon = + !warn || (invalid && warn) + ? undefined + : WarningAltFilled16({ + class: `${prefix}--list-box__invalid-icon ${prefix}--list-box__invalid-icon--warning`, + 'aria-label': toggleLabel, + }); + const helperMessage = invalid ? invalidText : warn ? warnText : helperText; const menuBody = !open ? undefined : html`