diff --git a/web-components/packages/carbon-web-components/src/components/data-table/table-toolbar-search.ts b/web-components/packages/carbon-web-components/src/components/data-table/table-toolbar-search.ts index 7b0090c8bd02..02a9b6dc5b67 100644 --- a/web-components/packages/carbon-web-components/src/components/data-table/table-toolbar-search.ts +++ b/web-components/packages/carbon-web-components/src/components/data-table/table-toolbar-search.ts @@ -14,7 +14,7 @@ import { prefix } from '../../globals/settings'; import HostListenerMixin from '../../globals/mixins/host-listener'; import HostListener from '../../globals/decorators/host-listener'; import { INPUT_SIZE } from '../text-input/text-input'; -import BXSearch from '../search/search'; +import CDSSearch from '../search/search'; import styles from './data-table.scss'; /** @@ -24,7 +24,7 @@ import styles from './data-table.scss'; * @fires cds-search-input - The custom event fired after the search content is changed upon a user gesture. */ @customElement(`${prefix}-table-toolbar-search`) -class BXTableToolbarSearch extends HostListenerMixin(BXSearch) { +class BXTableToolbarSearch extends HostListenerMixin(CDSSearch) { @query('input') private _inputNode!: HTMLInputElement; diff --git a/web-components/packages/carbon-web-components/src/components/search/defs.ts b/web-components/packages/carbon-web-components/src/components/search/defs.ts deleted file mode 100644 index e154c620dc68..000000000000 --- a/web-components/packages/carbon-web-components/src/components/search/defs.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @license - * - * Copyright IBM Corp. 2020 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -export { FORM_ELEMENT_COLOR_SCHEME as SEARCH_COLOR_SCHEME } from '../../globals/shared-enums'; diff --git a/web-components/packages/carbon-web-components/src/components/search/search-skeleton.ts b/web-components/packages/carbon-web-components/src/components/search/search-skeleton.ts index 1367317f994c..206195bbb2c9 100644 --- a/web-components/packages/carbon-web-components/src/components/search/search-skeleton.ts +++ b/web-components/packages/carbon-web-components/src/components/search/search-skeleton.ts @@ -17,7 +17,7 @@ import styles from './search.scss'; * Skeleton of search. */ @customElement(`${prefix}-search-skeleton`) -class BXSearchSkeleton extends LitElement { +class CDSSearchSkeleton extends LitElement { /** * The search box size. Corresponds to the attribute with the same name. */ @@ -34,4 +34,4 @@ class BXSearchSkeleton extends LitElement { static styles = styles; } -export default BXSearchSkeleton; +export default CDSSearchSkeleton; diff --git a/web-components/packages/carbon-web-components/src/components/search/search-story.ts b/web-components/packages/carbon-web-components/src/components/search/search-story.ts index cc1a548b340d..b8c82f351fcf 100644 --- a/web-components/packages/carbon-web-components/src/components/search/search-story.ts +++ b/web-components/packages/carbon-web-components/src/components/search/search-story.ts @@ -9,85 +9,154 @@ import { html } from 'lit'; import { ifDefined } from 'lit/directives/if-defined.js'; -import { action } from '@storybook/addon-actions'; -import { boolean, select } from '@storybook/addon-knobs'; +import { boolean, number, select } from '@storybook/addon-knobs'; import textNullable from '../../../.storybook/knob-text-nullable'; import { INPUT_SIZE } from '../text-input/text-input'; -import { SEARCH_COLOR_SCHEME } from './search'; import './search-skeleton'; import storyDocs from './search-story.mdx'; import { prefix } from '../../globals/settings'; -const colorSchemes = { - [`Regular`]: null, - [`Light (${SEARCH_COLOR_SCHEME.LIGHT})`]: SEARCH_COLOR_SCHEME.LIGHT, -}; - const sizes = { - 'Regular size': null, [`Small size (${INPUT_SIZE.SMALL})`]: INPUT_SIZE.SMALL, + [`Medium size (${INPUT_SIZE.MEDIUM})`]: INPUT_SIZE.MEDIUM, [`Large size (${INPUT_SIZE.LARGE})`]: INPUT_SIZE.LARGE, }; -export const Default = (args) => { +const widthSliderOptions = { + range: true, + min: 300, + max: 800, + step: 50, +}; + +export const Default = () => { + return html` + + `; +}; + +export const Disabled = () => { + return html` + + `; +}; + +export const Expandable = () => { + return html` + + `; +}; + +export const ExpandableWithLayer = () => { + return html` + + + + + + + + + + `; +}; + +export const WithLayer = () => { + return html` + + + + + + + + + + `; +}; + +export const Playground = (args) => { const { - closeButtonAssistiveText, + autoComplete, + closeButtonLabelText, colorScheme, disabled, labelText, - name, placeholder, + playgroundWidth, size, + role, type, value, onInput, } = args?.[`${prefix}-search`] ?? {}; + + const mainDiv = document.querySelector('#main-content'); + + if (mainDiv) { + (mainDiv as HTMLElement).style.width = `${playgroundWidth}px`; + } + return html` + @cds-search-input="${onInput}"> + `; }; -Default.storyName = 'Default'; - -Default.parameters = { +Playground.parameters = { knobs: { [`${prefix}-search`]: () => ({ - closeButtonAssistiveText: textNullable( - 'The label text for the close button (close-button-assistive-text)', + autoComplete: textNullable('Autocomplete (autocomplete)', 'off'), + closeButtonLabelText: textNullable( + 'The label text for the close button (close-button-label-text)', 'Clear search input' ), - colorScheme: select('Color scheme (color-scheme)', colorSchemes, null), disabled: boolean('Disabled (disabled)', false), labelText: textNullable('Label text (label-text)', 'Search'), - name: textNullable('Name (name)', ''), - placeholder: textNullable('Placeholder text (placeholder)', ''), + placeholder: textNullable( + 'Placeholder text (placeholder)', + 'Placeholder text' + ), + playgroundWidth: number('Playground width', 300, widthSliderOptions), + role: textNullable('The role of the (role)', 'searchbox'), size: select('Search size (size)', sizes, null), - type: textNullable('The type of the (type)', ''), + type: textNullable('The type of the (type)', 'text'), value: textNullable('Value (value)', ''), - onInput: action(`${prefix}-search-input`), }), }, }; -export const skeleton = () => - html` `; - -skeleton.parameters = { - percy: { - skip: true, - }, -}; - export default { title: 'Components/Search', parameters: { diff --git a/web-components/packages/carbon-web-components/src/components/search/search.scss b/web-components/packages/carbon-web-components/src/components/search/search.scss index 54327cb9a8e1..cba8494273df 100644 --- a/web-components/packages/carbon-web-components/src/components/search/search.scss +++ b/web-components/packages/carbon-web-components/src/components/search/search.scss @@ -8,6 +8,8 @@ $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/form'; @use '@carbon/styles/scss/components/search' as *; @@ -15,35 +17,51 @@ $css--plex: true !default; // https://github.com/carbon-design-system/carbon/issues/11408 @include search; -:host(#{$prefix}-search), -:host(#{$prefix}-search-skeleton) { - @extend .#{$prefix}--search--lg; -} - :host(#{$prefix}-search) { @extend .#{$prefix}--search; outline: none; -} -:host(#{$prefix}-search[size='sm']), -:host(#{$prefix}-search-skeleton[size='sm']) { - @extend .#{$prefix}--search--sm; -} + &[expandable] { + @extend .#{$prefix}--search--expandable; -:host(#{$prefix}-search[size='lg']), -:host(#{$prefix}-search-skeleton[size='lg']) { - @extend .#{$prefix}--search--lg; -} + &[expanded] { + @extend .#{$prefix}--search--expanded; -// TODO: deprecate -// :host(#{$prefix}-search[size='xl']), -// :host(#{$prefix}-search-skeleton[size='xl']) { -// @extend .#{$prefix}--search--xl; -// } + &[size='sm'] { + .#{$prefix}--search-input { + padding: 0 $spacing-07; + } + } + + &[size='md'] { + .#{$prefix}--search-input { + padding: 0 $spacing-08; + } + } + + &[size='lg'] { + .#{$prefix}--search-input { + padding: 0 $spacing-09; + } + } + } + } + + &[disabled] { + svg { + fill: $icon-on-color-disabled; + } -:host(#{$prefix}-search[color-scheme='light']) { - @extend .#{$prefix}--search--light; + .#{$prefix}--search-close { + pointer-events: none; + outline: none; + + &::before { + background: none; + } + } + } } :host(#{$prefix}-search-skeleton) { @@ -59,3 +77,18 @@ $css--plex: true !default; } } } + +:host(#{$prefix}-search), +:host(#{$prefix}-search-skeleton) { + &[size='sm'] { + @extend .#{$prefix}--search--sm; + } + + &[size='md'] { + @extend .#{$prefix}--search--md; + } + + &[size='lg'] { + @extend .#{$prefix}--search--lg; + } +} diff --git a/web-components/packages/carbon-web-components/src/components/search/search.ts b/web-components/packages/carbon-web-components/src/components/search/search.ts index 226d2422d78c..7415cedd25f0 100644 --- a/web-components/packages/carbon-web-components/src/components/search/search.ts +++ b/web-components/packages/carbon-web-components/src/components/search/search.ts @@ -11,18 +11,16 @@ import { classMap } from 'lit/directives/class-map.js'; import { LitElement, html } from 'lit'; import { property, customElement } from 'lit/decorators.js'; import Close16 from '@carbon/icons/lib/close/16'; -import Close20 from '@carbon/icons/lib/close/20'; import Search16 from '@carbon/icons/lib/search/16'; import { prefix } from '../../globals/settings'; import ifNonEmpty from '../../globals/directives/if-non-empty'; import FocusMixin from '../../globals/mixins/focus'; import FormMixin from '../../globals/mixins/form'; import { INPUT_SIZE } from '../text-input/text-input'; -import { SEARCH_COLOR_SCHEME } from './defs'; +import HostListener from '../../globals/decorators/host-listener'; +import HostListenerMixin from '../../globals/mixins/host-listener'; import styles from './search.scss'; -export { SEARCH_COLOR_SCHEME }; - /** * Search box. * @@ -35,7 +33,7 @@ export { SEARCH_COLOR_SCHEME }; * @fires cds-search-input - The custom event fired after the search content is changed upon a user gesture. */ @customElement(`${prefix}-search`) -class BXSearch extends FocusMixin(FormMixin(LitElement)) { +class CDSSearch extends HostListenerMixin(FocusMixin(FormMixin(LitElement))) { /** * Handles `input` event on the `` in the shadow DOM. */ @@ -43,7 +41,7 @@ class BXSearch extends FocusMixin(FormMixin(LitElement)) { const { target } = event; const { value } = target as HTMLInputElement; this.dispatchEvent( - new CustomEvent((this.constructor as typeof BXSearch).eventInput, { + new CustomEvent((this.constructor as typeof CDSSearch).eventInput, { bubbles: true, composed: true, cancelable: false, @@ -61,7 +59,7 @@ class BXSearch extends FocusMixin(FormMixin(LitElement)) { private _handleClearInputButtonClick() { if (this.value) { this.dispatchEvent( - new CustomEvent((this.constructor as typeof BXSearch).eventInput, { + new CustomEvent((this.constructor as typeof CDSSearch).eventInput, { bubbles: true, composed: true, cancelable: false, @@ -78,6 +76,42 @@ class BXSearch extends FocusMixin(FormMixin(LitElement)) { } } + /** + * Handles `focus` event on the button when the button can be expanded + */ + @HostListener('focus') + // @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to + private _handleExpand() { + if (this.expandable && !this.expanded) { + this.setAttribute('expanded', ''); + } + } + + /** + * Handles `focusout` event on the component to be closed after being expanded + * Will not close if there is a value typed within. + */ + @HostListener('focusout') + // @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to + private _handleClose() { + if (this.expandable && this.expanded && !this.value) { + this.removeAttribute('expanded'); + } + } + + /** + * Handler for @slotchange, will only be ran if user sets an element under the "icon" slot. + * + * @private + */ + private _handleSlotChange() { + const icon = this.querySelector('svg'); + icon?.setAttribute('part', 'search-icon'); + icon?.setAttribute('class', `${prefix}--search-magnifier-icon`); + icon?.setAttribute('role', `img`); + this.hasCustomIcon = true; + } + _handleFormdata(event: Event) { const { formData } = event as any; // TODO: Wait for `FormDataEvent` being available in `lib.dom.d.ts` const { disabled, name, value } = this; @@ -87,16 +121,17 @@ class BXSearch extends FocusMixin(FormMixin(LitElement)) { } /** - * The assistive text for the close button. + * Specify an optional value for the autocomplete property on the underlying , + * defaults to "off" */ - @property({ attribute: 'close-button-assistive-text' }) - closeButtonAssistiveText = ''; + @property({ attribute: 'autocomplete' }) + autoComplete = 'off'; /** - * The color scheme. + * Specify a label to be read by screen readers on the "close" button */ - @property({ attribute: 'color-scheme', reflect: true }) - colorScheme = SEARCH_COLOR_SCHEME.REGULAR; + @property({ attribute: 'close-button-label-text' }) + closeButtonLabelText = ''; /** * `true` if the search box should be disabled. @@ -104,6 +139,21 @@ class BXSearch extends FocusMixin(FormMixin(LitElement)) { @property({ type: Boolean, reflect: true }) disabled = false; + /** + * `true` if the search bar can be expandable + */ + @property({ type: Boolean, reflect: true }) + expandable = false; + + /** + * `true` if the expandable search has been expanded + */ + @property({ type: Boolean, reflect: true }) + expanded = false; + + @property({ type: Boolean }) + hasCustomIcon = false; + /** * The label text. */ @@ -116,11 +166,17 @@ class BXSearch extends FocusMixin(FormMixin(LitElement)) { @property() name = ''; + /** + * Specify the role for the underlying , defaults to searchbox + */ + @property() + role = ''; + /** * The placeholder text. */ @property() - placeholder = ''; + placeholder = 'Search'; /** * The search box size. @@ -142,31 +198,41 @@ class BXSearch extends FocusMixin(FormMixin(LitElement)) { render() { const { - closeButtonAssistiveText, + autoComplete, + closeButtonLabelText, disabled, + hasCustomIcon, labelText, name, placeholder, - size, + role, type, value = '', _handleInput: handleInput, _handleClearInputButtonClick: handleClearInputButtonClick, + _handleSlotChange: handleSlotChange, } = this; const clearClasses = classMap({ [`${prefix}--search-close`]: true, [`${prefix}--search-close--hidden`]: !this.value, }); return html` - ${Search16({ - part: 'search-icon', - class: `${prefix}--search-magnifier-icon`, - role: 'img', - })} +
+ + ${hasCustomIcon + ? html`` + : html`${Search16({ + part: 'search-icon', + class: `${prefix}--search-magnifier-icon`, + role: 'img', + })}`} + +
@@ -206,4 +272,4 @@ class BXSearch extends FocusMixin(FormMixin(LitElement)) { static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader } -export default BXSearch; +export default CDSSearch; diff --git a/web-components/packages/carbon-web-components/tests/spec/search_spec.ts b/web-components/packages/carbon-web-components/tests/spec/search_spec.ts index 987faeec22c8..5686b44c7a51 100644 --- a/web-components/packages/carbon-web-components/tests/spec/search_spec.ts +++ b/web-components/packages/carbon-web-components/tests/spec/search_spec.ts @@ -10,13 +10,11 @@ import { render } from 'lit'; import EventManager from '../utils/event-manager'; import { INPUT_SIZE } from '../../src/components/text-input/text-input'; -import BXSearch, { - SEARCH_COLOR_SCHEME, -} from '../../src/components/search/search'; -import { Default } from '../../src/components/search/search-story'; +import CDSSearch from '../../src/components/search/search'; +import { Playground } from '../../src/components/search/search-story'; const template = (props?) => - Default({ + Playground({ 'cds-search': props, }); @@ -36,7 +34,6 @@ describe('cds-search', function () { render( template({ closeButtonAssistiveText: 'close-button-assistive-text-foo', - colorScheme: SEARCH_COLOR_SCHEME.LIGHT, disabled: true, labelText: 'label-text-foo', name: 'name-foo', @@ -62,7 +59,7 @@ describe('cds-search', function () { const inputNode = search!.shadowRoot!.querySelector('input'); inputNode!.value = 'value-bar'; inputNode!.dispatchEvent(new CustomEvent('input', { bubbles: true })); - expect((search as BXSearch).value).toBe('value-bar'); + expect((search as CDSSearch).value).toBe('value-bar'); }); it('Should fire cds-search-input event upon typing', async function () { @@ -86,7 +83,7 @@ describe('cds-search', function () { await Promise.resolve(); const search = document.body.querySelector('cds-search'); search!.shadowRoot!.querySelector('button')!.click(); - expect((search as BXSearch).value).toBe(''); + expect((search as CDSSearch).value).toBe(''); }); it('Should fire cds-search-input event upon clearing', async function () {