diff --git a/packages/web-components/src/components/footer/__stories__/README.stories.mdx b/packages/web-components/src/components/footer/__stories__/README.stories.mdx index 49aeebe67c6..63e399b53fe 100644 --- a/packages/web-components/src/components/footer/__stories__/README.stories.mdx +++ b/packages/web-components/src/components/footer/__stories__/README.stories.mdx @@ -51,4 +51,42 @@ See [our README](https://github.com/carbon-design-system/carbon-for-ibm-dotcom/t | `` | `data-autoid="dds--legal-nav"` | Component | | `` | `data-autoid="dds--privacy-cp"` | Component | +### Language selector + +The option to use a language selector is available in lieu of the locale +button/selector. + +The following attributes are needed: + +| Attribute | Description | +| ------------------------ | -------------------------------------- | +| `language-selector-label`| `The input box placeholder` | +| `selected-language` | `The initial selected language` | +| `localeList` | `Object containing available languages`| + +Example implementation: + +```javascript +const languageList = [ + { id: 'da', text: 'Danish / Dansk' }, + { id: 'nl', text: 'Dutch / Nederlands' }, + { id: 'en', text: 'English' }, +]; + +``` + +```html + + +``` diff --git a/packages/web-components/src/components/footer/__stories__/footer.stories.ts b/packages/web-components/src/components/footer/__stories__/footer.stories.ts index 17877d975c5..87b8822001b 100644 --- a/packages/web-components/src/components/footer/__stories__/footer.stories.ts +++ b/packages/web-components/src/components/footer/__stories__/footer.stories.ts @@ -1,7 +1,7 @@ /** * @license * - * Copyright IBM Corp. 2020 + * Copyright IBM Corp. 2020, 2021 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. @@ -13,24 +13,34 @@ import inPercy from '@percy-io/in-percy'; import { FOOTER_SIZE } from '../footer'; import '../footer-composite'; import '../footer-container'; +import mockLangList from './language-list'; import mockLinks from './links'; import mockLegalLinks from './legal-links'; import mockLocaleList from '../../locale-modal/__stories__/locale-data.json'; import readme from './README.stories.mdx'; +import styles from './footer.stories.scss'; export const base = ({ parameters }) => { - const { langDisplay, language, size, legalLinks, links, localeList } = parameters?.props?.FooterComposite ?? {}; + const { langDisplay, language, size, langList, legalLinks, links, localeList, languageSelectorLabel, selectedLanguage } = + parameters?.props?.FooterComposite ?? {}; const { useMock } = parameters?.props?.Other ?? {}; + return html` + ${useMock ? html` ` @@ -39,9 +49,12 @@ export const base = ({ parameters }) => { language="${ifNonNull(language)}" lang-display="${ifNonNull(langDisplay)}" size="${ifNonNull(size)}" + .langList="${ifNonNull(langList)}" .legalLinks="${ifNonNull(legalLinks)}" .links="${ifNonNull(links)}" .localeList="${ifNonNull(localeList)}" + language-selector-label="${ifNonNull(languageSelectorLabel)}" + selected-language="${ifNonNull(selectedLanguage)}" > `} @@ -53,6 +66,19 @@ export const Default = ({ parameters }) => { props.FooterComposite = { ...(props.FooterComposite || {}), size: FOOTER_SIZE.REGULAR, + langList: '', + }; + return base({ parameters }); +}; + +export const defaultLanguageOnly = ({ parameters }) => { + const { props = {} } = parameters; + props.FooterComposite = { + ...(props.FooterComposite || {}), + size: FOOTER_SIZE.REGULAR, + languageSelectorLabel: 'Choose a language', + selectedLanguage: 'English', + langList: mockLangList, }; return base({ parameters }); }; @@ -62,10 +88,27 @@ export const short = ({ parameters }) => { props.FooterComposite = { ...(props.FooterComposite || {}), size: FOOTER_SIZE.SHORT, + langList: '', }; return base({ parameters }); }; +export const shortDefaultLanguageOnly = ({ parameters }) => { + const { props = {} } = parameters; + props.FooterComposite = { + ...(props.FooterComposite || {}), + size: FOOTER_SIZE.SHORT, + languageSelectorLabel: 'Choose a language', + selectedLanguage: 'English', + langList: mockLangList, + }; + return html` +
+ ${base({ parameters })} +
+ `; +}; + export const micro = ({ parameters }) => { const { props = {} } = parameters; props.FooterComposite = { diff --git a/packages/web-components/src/components/footer/__stories__/language-list.ts b/packages/web-components/src/components/footer/__stories__/language-list.ts new file mode 100644 index 00000000000..2448640ec7f --- /dev/null +++ b/packages/web-components/src/components/footer/__stories__/language-list.ts @@ -0,0 +1,49 @@ +/** + * @license + * + * Copyright IBM Corp. 2020, 2021 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +const languageList: string[] = [ + 'Arabic / عربية', + 'Bosnian / Bosanski', + 'Bulgarian / Български', + 'Catalan / Català', + 'Chinese Simplified / 简体中文', + 'Chinese Traditional / 繁體中文', + 'Croatian / Hrvatski', + 'Czech / Čeština', + 'Danish / Dansk', + 'Dutch / Nederlands', + 'English', + 'Finnish / Suomi', + 'French / Français', + 'German / Deutsch', + 'Greek / Ελληνικά', + 'Hebrew / עברית', + 'Hungarian / Magyar', + 'Italian / Italiano', + 'Japanese / 日本語', + 'Kazakh / Қазақша', + 'Korean / 한국어', + 'Macedonian / македонски', + 'Norwegian / Norsk', + 'Polish / polski', + 'Portuguese/Brazil/Brazil / Português/Brasil', + 'Portuguese/Portugal / Português/Portugal', + 'Romanian / Română', + 'Russian / Русский', + 'Serbian / srpski', + 'Slovak / Slovenčina', + 'Slovenian / Slovenščina', + 'Spanish / Español', + 'Swedish / Svenska', + 'Thai / ภาษาไทย', + 'Turkish / Türkçe', + 'Vietnamese / Tiếng Việt', +]; + +export default languageList; diff --git a/packages/web-components/src/components/footer/defs.ts b/packages/web-components/src/components/footer/defs.ts index d970ce0ec7d..c175962b530 100644 --- a/packages/web-components/src/components/footer/defs.ts +++ b/packages/web-components/src/components/footer/defs.ts @@ -1,7 +1,7 @@ /** * @license * - * Copyright IBM Corp. 2020 + * Copyright IBM Corp. 2020, 2021 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. @@ -26,3 +26,19 @@ export enum FOOTER_SIZE { */ MICRO = 'micro', } + +/** + * The style scheme of language selector. + */ +export enum LANGUAGE_SELECTOR_STYLE_SCHEME { + /** + * Regular style scheme. + */ + REGULAR = '', + + /** + * The style scheme that's blendid into outer style scheme. + * The primary use case is for used in micro footer. + */ + BLENDED = 'blended', +} diff --git a/packages/web-components/src/components/footer/footer-composite.ts b/packages/web-components/src/components/footer/footer-composite.ts index fe178447e19..84a03fed883 100644 --- a/packages/web-components/src/components/footer/footer-composite.ts +++ b/packages/web-components/src/components/footer/footer-composite.ts @@ -1,7 +1,7 @@ /** * @license * - * Copyright IBM Corp. 2020 + * Copyright IBM Corp. 2020, 2021 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. @@ -35,6 +35,8 @@ import './locale-button'; import './legal-nav'; import './legal-nav-item'; import './legal-nav-cookie-preferences-placeholder'; +import './language-selector'; +import 'carbon-web-components/es/components/combo-box/combo-box-item'; const { stablePrefix: ddsPrefix } = ddsSettings; @@ -115,12 +117,36 @@ class DDSFooterComposite extends ModalRenderMixin(HybridRenderMixin(HostListener @property() language?: string; + /** + * Placeholder list of languages to populate language selector + * + * @internal + */ + @property({ attribute: false }) + langList?: string[]; + /** * The language to show in the UI. */ @property({ attribute: 'lang-display' }) langDisplay?: string; + /** + * The placeholder label for language selector. + * + * @internal + */ + @property({ attribute: 'language-selector-label' }) + languageSelectorLabel?: string; + + /** + * The initial selected language in the selector. + * + * @internal + */ + @property({ attribute: 'selected-language' }) + selectedLanguage?: string; + /** * The legal nav links. */ @@ -203,6 +229,9 @@ class DDSFooterComposite extends ModalRenderMixin(HybridRenderMixin(HostListener buttonLabel, disableLocaleButton, langDisplay, + langList, + languageSelectorLabel, + selectedLanguage, size, links, legalLinks, @@ -224,13 +253,21 @@ class DDSFooterComposite extends ModalRenderMixin(HybridRenderMixin(HostListener ` )} - ${!disableLocaleButton && size !== FOOTER_SIZE.MICRO + ${!disableLocaleButton && size !== FOOTER_SIZE.MICRO && !langList ? html` ${langDisplay} ` - : html``} + : html` + + ${langList?.map( + language => html` + ${ifNonNull(language)} + ` + )} + + `} ${legalLinks?.map( ({ title, url }) => html` diff --git a/packages/web-components/src/components/footer/footer-container.ts b/packages/web-components/src/components/footer/footer-container.ts index cc167a5a771..0545f157df5 100644 --- a/packages/web-components/src/components/footer/footer-container.ts +++ b/packages/web-components/src/components/footer/footer-container.ts @@ -1,7 +1,7 @@ /** * @license * - * Copyright IBM Corp. 2020 + * Copyright IBM Corp. 2020, 2021 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. diff --git a/packages/web-components/src/components/footer/footer.scss b/packages/web-components/src/components/footer/footer.scss index e7fcdd236b2..003576cf192 100644 --- a/packages/web-components/src/components/footer/footer.scss +++ b/packages/web-components/src/components/footer/footer.scss @@ -1,5 +1,5 @@ // -// Copyright IBM Corp. 2020 +// Copyright IBM Corp. 2020, 2021 // // This source code is licensed under the Apache-2.0 license found in the // LICENSE file in the root directory of this source tree. @@ -9,6 +9,9 @@ @import 'carbon-components/scss/globals/scss/vendor/@carbon/elements/scss/grid/mixins'; @import 'carbon-components/scss/globals/scss/vars'; @import 'carbon-components/scss/components/accordion/accordion'; +@import 'carbon-components/scss/components/combo-box/combo-box'; +@import 'carbon-components/scss/components/form/form'; +@import 'carbon-components/scss/components/text-input/text-input'; @import 'carbon-components/scss/components/link/link'; @import '@carbon/ibmdotcom-styles/scss/globals/vars'; @import '@carbon/ibmdotcom-styles/scss/themes/expressive/tokens'; @@ -27,6 +30,7 @@ :host(#{$dds-prefix}-footer-nav-group), :host(#{$dds-prefix}-footer-nav), :host(#{$dds-prefix}-locale-button), +:host(#{$dds-prefix}-language-selector), .#{$prefix}--legal-nav { // Overrides productive type tokens set by `g90` theme zoning @include use-carbon-expressive-tokens(); @@ -61,6 +65,27 @@ } } +:host(#{$dds-prefix}-footer[size='short']) { + ::slotted(#{$dds-prefix}-language-selector) { + display: block; + margin-top: 0; + + @include carbon--breakpoint('sm') { + width: 100%; + } + + @include carbon--breakpoint('md') { + flex: 0 0 50%; + margin-left: 25%; + } + + @include carbon--breakpoint('lg') { + flex: 0 0 25%; + margin-left: 62.5%; + } + } +} + // The style of legal nav deviates from the one of React, so we can make the markup simpler. // FIXME: Once the style is stabilized, change the markup of React, too, so we can share the style :host(#{$dds-prefix}-legal-nav):not([size='micro']) { @@ -87,7 +112,8 @@ :host(#{$dds-prefix}-footer[size='micro']) { ::slotted(#{$dds-prefix}-footer-nav), - ::slotted(#{$dds-prefix}-footer-logo) { + ::slotted(#{$dds-prefix}-footer-logo), + ::slotted(#{$dds-prefix}-language-selector) { display: none; } } @@ -133,16 +159,16 @@ margin-right: $carbon--spacing-05; } - .bx--btn { + .#{$prefix}--btn { padding-top: $carbon--spacing-04; padding-bottom: $carbon--spacing-04; } - .bx--locale-btn { + .#{$prefix}--locale-btn { max-width: 100%; } - .bx--btn--secondary { + .#{$prefix}--btn--secondary { background-color: $ui-background; margin: 0; @@ -181,3 +207,36 @@ } } } + +:host(#{$dds-prefix}-language-selector) { + position: relative; + + height: $carbon--spacing-09; + margin: $carbon--spacing-05 0; + + outline: none; + + @include carbon--breakpoint('sm') { + width: auto; + } + + @include carbon--breakpoint('md') { + width: 33%; + } + + @include carbon--breakpoint('lg') { + order: 1; + flex: 0 0 18.75%; + } + + .#{$prefix}--combo-box { + bottom: $carbon--spacing-05; + } + + .#{$prefix}--list-box__menu { + position: absolute; + top: auto; + bottom: $carbon--spacing-08; + max-height: 13.5rem; + } +} diff --git a/packages/web-components/src/components/footer/footer.ts b/packages/web-components/src/components/footer/footer.ts index b7b7791af8b..450bde9ac97 100644 --- a/packages/web-components/src/components/footer/footer.ts +++ b/packages/web-components/src/components/footer/footer.ts @@ -1,7 +1,7 @@ /** * @license * - * Copyright IBM Corp. 2020 + * Copyright IBM Corp. 2020, 2021 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. @@ -49,6 +49,7 @@ class DDSFooter extends StableSelectorMixin(LitElement) { + diff --git a/packages/web-components/src/components/footer/language-selector.ts b/packages/web-components/src/components/footer/language-selector.ts new file mode 100644 index 00000000000..e6028e7ba2d --- /dev/null +++ b/packages/web-components/src/components/footer/language-selector.ts @@ -0,0 +1,146 @@ +/** + * @license + * + * Copyright IBM Corp. 2020, 2021 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { property, customElement, query, internalProperty } from 'lit-element'; +import ddsSettings from '@carbon/ibmdotcom-utilities/es/utilities/settings/settings.js'; +import BXComboBox, { DROPDOWN_SIZE } from 'carbon-web-components/es/components/combo-box/combo-box.js'; +import BXComboBoxItem from 'carbon-web-components/es/components/combo-box/combo-box-item.js'; +import HostListener from 'carbon-web-components/es/globals/decorators/host-listener.js'; +import HostListenerMixin from 'carbon-web-components/es/globals/mixins/host-listener.js'; +import { LANGUAGE_SELECTOR_STYLE_SCHEME } from './defs'; +import styles from './footer.scss'; +import { findIndex, forEach } from '../../globals/internal/collection-helpers'; + +const { stablePrefix: ddsPrefix } = ddsSettings; + +/** + * Language selector component. + * The API for language selection is still subject to change. + * + * @element dds-language-selector + * @internal + */ +@customElement(`${ddsPrefix}-language-selector`) +class DDSLanguageSelector extends HostListenerMixin(BXComboBox) { + /** + * Property that saves the last valid language to use on reset cases. + */ + @internalProperty() + private _lastValidLang?: string; + + /** + * The `` for filtering. + */ + @query('input') + private _filterInputNode!: HTMLInputElement; + + /** + * Reverts input value to last chosen valid language. + * + * @param event the event + */ + @HostListener('document:click') + protected _handleClickOutside = (event: MouseEvent) => { + if (!this.contains(event.target as Node)) { + this._filterInputValue = this._lastValidLang as string; + this._filterInputNode.value = this._lastValidLang as string; + this.open = false; + this.requestUpdate(); // If the only change is to `_filterInputValue`, auto-update doesn't happen + } + }; + + /** + * Handles `input` event on the `` for filtering. + * Highlights and scrolls into the view the matched item. + */ + protected _handleInput() { + const items = this.querySelectorAll((this.constructor as typeof BXComboBox).selectorItem); + const index = !this._filterInputNode.value ? -1 : findIndex(items, this._testItemWithQueryText, this); + forEach(items, (item, i) => { + if (i === index) item.scrollIntoView(); + (item as BXComboBoxItem).highlighted = i === index; + }); + const { _filterInputNode: filterInput } = this; + this._filterInputValue = !filterInput ? '' : filterInput.value; + this.open = true; + this.requestUpdate(); // If the only change is to `_filterInputValue`, auto-update doesn't happen + } + + /** + * Handles user-initiated clearing the `` for filtering. + * Also saves the current valid language. + */ + protected _handleUserInitiatedClearInput() { + forEach(this.querySelectorAll((this.constructor as typeof BXComboBox).selectorItem), item => { + (item as BXComboBoxItem).highlighted = false; + (item as BXComboBoxItem).selected = false; + }); + this._lastValidLang = this._filterInputValue; + this._filterInputValue = ''; + this._filterInputNode.focus(); + this.open = false; + this.requestUpdate(); + } + + /** + * Updates the input and last valid language to the selected item text. + * + * @param item that was selected + */ + protected _handleUserInitiatedSelectItem(item?: BXComboBoxItem) { + 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. + // Use case: + // 1. Select the 2nd item in combo box drop down + // 2. Type some text in the `` + // 3. Re-select the 2nd item in combo box drop down, + // the `` has to updated with the 2nd item and the dropdown should be closed, + // even if there is no change in the selected value + this._filterInputValue = item.textContent || ''; + this.open = false; + this.requestUpdate(); + } + this._lastValidLang = item?.textContent as string; + (item as BXComboBoxItem).selected = true; + super._handleUserInitiatedSelectItem(item); + } + + /** + * Property that specifies the ComboBox to have size xl + * + * @internal + */ + @property() + size = DROPDOWN_SIZE.EXTRA_LARGE; + + /** + * Size property to apply different styles. + */ + @property({ attribute: 'style-scheme' }) + styleScheme = LANGUAGE_SELECTOR_STYLE_SCHEME.REGULAR; + + /** + * The shadow slot this language-selector should be in. + */ + @property({ reflect: true }) + slot = 'language-selector'; + + // @ts-ignore + updated(changedProperties) { + super.updated(); + if (changedProperties.has('value')) { + this._lastValidLang = this.value; + } + } + + static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader +} + +export default DDSLanguageSelector;