UNPKG

sober

Version:

Sober is a library of UI components designed with reference to Material You.

219 lines (218 loc) 6.58 kB
import { useElement, useProps } from './core/element.js'; import { convertCSSDuration } from './core/utils/CSSUtils.js'; import { Select } from './core/utils/select.js'; import './ripple.js'; const name = 's-tab'; const props = useProps({ mode: ['scrollable', 'fixed'], $value: '' }); const style = /*css*/ ` :host{ display: flex; justify-content: flex-start; position: relative; background: var(--s-color-surface, ${"#F8F9FB" /* Theme.colorSurface */}); color: var(--s-color-on-surface-variant, ${"#40484C" /* Theme.colorOnSurfaceVariant */}); } :host::before{ content: ''; position: absolute; width: 100%; border-bottom: solid 1px var(--s-color-surface-variant, ${"#DCE4E8" /* Theme.colorSurfaceVariant */}); bottom: 0; left: 0; } .container{ display: flex; justify-content: flex-start; align-items: center; position: relative; scrollbar-width: none; overflow-x: auto; } .container::-webkit-scrollbar{ display: none; } :host([mode=fixed]) .container{ overflow: hidden; width: 100%; } ::slotted(s-tab-item){ flex-shrink: 0; flex-basis: auto; } :host([mode=fixed]) ::slotted(s-tab-item){ flex-basis: 100%; flex-shrink: 1; } `; const template = /*html*/ ` <div class="container" part="container"> <slot></slot> </div> `; export class Tab extends useElement({ style, template, props, setup(shadowRoot) { const slot = shadowRoot.querySelector('slot'); const container = shadowRoot.querySelector('.container'); const select = new Select({ context: this, class: TabItem, slot }); const computedStyle = getComputedStyle(this); const getAnimateOptions = () => { const easing = computedStyle.getPropertyValue('--s-motion-easing-standard') || "cubic-bezier(0.2, 0, 0, 1.0)" /* Theme.motionEasingStandard */; const duration = computedStyle.getPropertyValue('--s-motion-duration-medium4') || "400ms" /* Theme.motionDurationMedium4 */; return { easing: easing, duration: convertCSSDuration(duration) }; }; select.onUpdate = (old) => { if (select.select && container.scrollWidth !== container.offsetWidth) { const left = (select.select.offsetLeft - container.offsetLeft) - (container.offsetWidth / 2 - select.select.offsetWidth / 2); container.scrollTo({ left, behavior: 'smooth' }); } if (!old || !select.select || !this.isConnected) return; const oldRect = old.shadowRoot.querySelector('.indicator').getBoundingClientRect(); const indicator = select.select.shadowRoot?.querySelector('.indicator'); const rect = indicator.getBoundingClientRect(); const offset = oldRect.left - rect.left; indicator.style.transform = `translateX(${rect.left > oldRect.left ? offset : Math.abs(offset)}px)`; indicator.style.width = `${oldRect.width}px`; const animation = indicator.animate([{ transform: `translateX(0)`, width: `${rect.width}px` }], getAnimateOptions()); animation.onfinish = animation.oncancel = animation.onremove = () => { indicator.style.removeProperty('transform'); indicator.style.removeProperty('width'); }; }; return { expose: { get options() { return select.list; }, get selectedIndex() { return select.selectedIndex; }, }, value: { get: () => select.value, set: (value) => select.value = value } }; } }) { } const itemName = 's-tab-item'; const itemProps = useProps({ selected: false, $value: '' }); const itemStyle = /*css*/ ` :host{ display: flex; align-items: center; justify-content: center; min-height: 48px; position: relative; cursor: pointer; font-size: .875rem; font-weight: 500; text-transform: capitalize; padding: 0 16px; } :host([selected=true]){ color: var(--s-color-primary, ${"#006782" /* Theme.colorPrimary */}); } .container{ display: flex; justify-content: center; align-items: center; flex-direction: column; position: relative; min-height: inherit; } .indicator{ position: absolute; bottom: 0; left: 0; height: 3px; width: 100%; background: var(--s-color-primary, ${"#006782" /* Theme.colorPrimary */}); border-radius: 1.5px 1.5px 0 0; opacity: 0; } :host([selected=true]) .indicator{ opacity: 1; } .text{ display: flex; align-items: center; line-height: 1; } ::slotted([slot=icon]){ width: 24px; height: 24px; color: currentColor; fill: currentColor; margin: 10px 0; } ::slotted([slot=text]){ white-space: nowrap; text-overflow: ellipsis; line-height: 1; } .icon ::slotted([slot=text]){ margin-top: -6px; height: 26px; } ::slotted([slot=badge]){ margin-left: 4px; } ::slotted([slot=badge]:not(:empty)){ width: auto; } .icon ::slotted([slot=badge]){ position: absolute; right: 0; width: 8px; top: 12px; margin-left: 0; } `; const itemTemplate = /*html*/ ` <div class="container" part="container"> <slot name="icon"></slot> <div class="text" part="text"> <slot name="text"></slot> <slot name="badge"></slot> </div> <div class="indicator" part="indicator"></div> </div> <s-ripple attached="true" part="ripple"></s-ripple> `; export class TabItem extends useElement({ style: itemStyle, template: itemTemplate, props: itemProps, setup(shadowRoot) { const container = shadowRoot.querySelector('.container'); shadowRoot.querySelector('[name=icon]').addEventListener('slotchange', (event) => { const slot = event.target; const length = slot.assignedElements().length; container.classList[length > 0 ? 'add' : 'remove']('icon'); }); this.addEventListener('click', () => { if (!(this.parentNode instanceof Tab) || this.selected) return; this.dispatchEvent(new Event(`${name}:select`, { bubbles: true })); }); return { selected: () => { if (!(this.parentNode instanceof Tab)) return; this.dispatchEvent(new Event(`${name}:update`, { bubbles: true })); }, }; } }) { } Tab.define(name); TabItem.define(itemName);