sober
Version:
Sober is a library of UI components designed with reference to Material You.
181 lines (180 loc) • 5.94 kB
JavaScript
import { useElement, useProps } from './core/element.js';
import { Select } from './core/utils/select.js';
import { convertCSSDuration } from './core/utils/CSSUtils.js';
import './ripple.js';
const name = 's-segmented-button';
const props = useProps({
$value: '',
mode: ['auto', 'fixed']
});
const style = /*css*/ `
:host{
display: inline-flex;
vertical-align: middle;
align-items: center;
border-radius: 20px;
height: 40px;
padding: 3px;
overflow: hidden;
box-sizing: border-box;
border: solid 1px var(--s-color-surface-variant, ${"#C0C8CC" /* Theme.colorOutlineVariant */});
background: var(--s-color-surface-container, ${"#ECEEF0" /* Theme.colorSurfaceContainer */});
}
:host([mode=fixed]){
display: flex;
}
:host([mode=fixed]) ::slotted(s-segmented-button-item){
flex-basis: 100%;
}
`;
const template = /*html*/ `<slot></slot>`;
export class SegmentedButton extends useElement({
style, template, props,
setup(shadowRoot) {
const slot = shadowRoot.querySelector('slot');
const select = new Select({ context: this, class: SegmentedButtonItem, 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 (!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`;
old.style.zIndex = '1';
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');
old.style.removeProperty('z-index');
};
};
return {
expose: {
get options() {
return select.list;
},
get selectedIndex() {
return select.selectedIndex;
},
},
value: {
get: () => select.value,
set: (value) => select.value = value
}
};
}
}) {
}
const itemName = 's-segmented-button-item';
const itemProps = useProps({
selected: false,
disabled: false,
selectable: true,
$value: ''
});
const itemStyle = /*css*/ `
:host{
display: flex;
justify-content: center;
align-items: center;
height: 100%;
min-width: 64px;
padding: 0 16px;
text-transform: capitalize;
cursor: pointer;
font-weight: 500;
font-size: .8125rem;
position: relative;
box-sizing: border-box;
border-radius: 20px;
transition: color var(--s-motion-duration-medium4, ${"400ms" /* Theme.motionDurationMedium4 */}) var(--s-motion-easing-standard, ${"cubic-bezier(0.2, 0, 0, 1.0)" /* Theme.motionEasingStandard */});
color: var(--s-color-on-surface, ${"#191C1E" /* Theme.colorOnSurface */});
}
:host([selected=true]){
color: var(--s-color-on-primary, ${"#ffffff" /* Theme.colorOnPrimary */});
}
:host([disabled=true]){
pointer-events: none;
color: color-mix(in srgb, var(--s-color-on-surface, ${"#191C1E" /* Theme.colorOnSurface */}) 38%, transparent);
}
.indicator{
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
opacity: 0;
border-radius: inherit;
background: var(--s-color-primary, ${"#006782" /* Theme.colorPrimary */});
}
:host([selected=true]) .indicator{
opacity: 1;
}
::slotted([slot]){
width: 18px;
height: 18px;
color: inherit;
fill: currentColor;
flex-shrink: 0;
position: relative;
}
::slotted([slot=start]){
margin-right: 4px;
}
::slotted([slot=end]){
margin-right: 4px;
}
.text{
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
position: relative;
}
not (color: color-mix(in srgb, black, white)){
:host([disabled=true]){
opacity: .38;
color: var(--s-color-on-surface, ${"#191C1E" /* Theme.colorOnSurface */});
}
}
`;
const itemTemplate = /*html*/ `
<div class="indicator" part="indicator"></div>
<slot name="start"></slot>
<div class="text" part="text">
<slot></slot>
</div>
<slot name="end"></slot>
<s-ripple attached="true" part="ripple"></s-ripple>
`;
export class SegmentedButtonItem extends useElement({
style: itemStyle,
template: itemTemplate,
props: itemProps,
setup() {
this.addEventListener('click', () => {
if (!(this.parentNode instanceof SegmentedButton) || this.selected)
return;
if (this.selectable)
this.dispatchEvent(new Event(`${name}:select`, { bubbles: true }));
});
return {
selected: () => {
if (!(this.parentNode instanceof SegmentedButton))
return;
this.dispatchEvent(new CustomEvent(`${name}:update`, { bubbles: true, detail: {} }));
}
};
}
}) {
}
SegmentedButton.define(name);
SegmentedButtonItem.define(itemName);