chore: move segmented button into the new directory

PiperOrigin-RevId: 461113111
This commit is contained in:
Material Web Team 2022-07-14 20:50:50 -07:00 committed by Copybara-Service
parent 5b2ec5e549
commit 88b164b00f
18 changed files with 1197 additions and 0 deletions

View File

@ -0,0 +1,142 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
@use 'sass:map';
@use '@material/web/sass/resolvers';
@use '@material/web/sass/theme';
@use '@material/web/tokens';
@use './segmented-button-theme';
$light-theme: tokens.md-comp-outlined-segmented-button-values();
$dark-theme: tokens.md-comp-outlined-segmented-button-values(
(
md-sys-color: tokens.md-sys-color-values-dark-dynamic(),
)
);
@mixin theme-styles($theme, $resolvers: resolvers.$material) {
$theme: theme.validate-theme-styles(
$light-theme,
$theme,
$require-all: false
);
$theme: segmented-button-theme.resolve-theme($theme, $resolvers);
$theme: theme.create-theme-vars($theme, 'segmented-button');
@include segmented-button-theme.unselected-ripple(
(
'hover-state-layer-color':
map.get($theme, 'unselected-hover-state-layer-color'),
'hover-state-layer-opacity': map.get($theme, 'hover-state-layer-opacity'),
'focus-state-layer-color':
map.get($theme, 'unselected-focus-state-layer-color'),
'focus-state-layer-opacity': map.get($theme, 'focus-state-layer-opacity'),
'pressed-state-layer-color':
map.get($theme, 'unselected-pressed-state-layer-color'),
'pressed-state-layer-opacity':
map.get($theme, 'pressed-state-layer-opacity'),
)
);
@include segmented-button-theme.selected-ripple(
(
'hover-state-layer-color':
map.get($theme, 'selected-hover-state-layer-color'),
'hover-state-layer-opacity': map.get($theme, 'hover-state-layer-opacity'),
'focus-state-layer-color':
map.get($theme, 'selected-focus-state-layer-color'),
'focus-state-layer-opacity': map.get($theme, 'focus-state-layer-opacity'),
'pressed-state-layer-color':
map.get($theme, 'selected-pressed-state-layer-color'),
'pressed-state-layer-opacity':
map.get($theme, 'pressed-state-layer-opacity'),
)
);
@include segmented-button-theme.container-color(
map.get($theme, 'selected-container-color')
);
@include segmented-button-theme.outline-colors(
(
'default': map.get($theme, 'outline-color'),
'disabled': map.get($theme, 'disabled-outline-color'),
)
);
@include segmented-button-theme.icon-size(
map.get($theme, 'with-icon-icon-size')
);
// Under the following conditions, we need to account for extra space between
// the graphic and the text label/icon content:
//
// 1. A button with an icon and a label.
// 2. A selected button with a label and checkmark.
// 3. A selected button with an icon and checkmark but no label.
//
// We calculate a larger width here instead of using padding or margin for
// two main reasons:
//
// 1. We may need to transition between a zero width and an explicit width.
// 2. Both margin and padding take up space when a node has child content
// even when a zero width is set and overflow is set to hidden.
//
// Because of those reasons, we calculate a new width with the given values.
@include segmented-button-theme.visible-graphic-width(
// TODO(b/198759625): Use padding token instead of hardcoded 8px value.
calc(map.get($theme, 'with-icon-icon-size') + 8px)
);
@include segmented-button-theme.unselected-icon-colors(
(
'default': map.get($theme, 'unselected-with-icon-icon-color'),
'hover': map.get($theme, 'unselected-hover-icon-color'),
'focus': map.get($theme, 'unselected-focus-icon-color'),
'pressed': map.get($theme, 'unselected-pressed-icon-color'),
'disabled': map.get($theme, 'disabled-icon-color'),
)
);
@include segmented-button-theme.selected-icon-colors(
(
'default': map.get($theme, 'selected-with-icon-icon-color'),
'hover': map.get($theme, 'selected-hover-icon-color'),
'focus': map.get($theme, 'selected-focus-icon-color'),
'pressed': map.get($theme, 'selected-pressed-icon-color'),
'disabled': map.get($theme, 'disabled-icon-color'),
)
);
@include segmented-button-theme.text-label-font(
(
'font': map.get($theme, 'label-text-font'),
'size': map.get($theme, 'label-text-size'),
'tracking': map.get($theme, 'label-text-tracking'),
'weight': map.get($theme, 'label-text-weight'),
)
);
@include segmented-button-theme.selected-text-label-color(
(
'default': map.get($theme, 'selected-label-text-color'),
'hover': map.get($theme, 'selected-hover-label-text-color'),
'focus': map.get($theme, 'selected-focus-label-text-color'),
'pressed': map.get($theme, 'selected-pressed-label-text-color'),
)
);
@include segmented-button-theme.unselected-text-label-color(
(
'default': map.get($theme, 'unselected-label-text-color'),
'hover': map.get($theme, 'unselected-hover-label-text-color'),
'focus': map.get($theme, 'unselected-focus-label-text-color'),
'pressed': map.get($theme, 'unselected-pressed-label-text-color'),
'disabled': map.get($theme, 'disabled-label-text-color'),
)
);
}

View File

@ -0,0 +1,19 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// stylelint-disable selector-class-pattern -- allow `md3-` class selectors.
@mixin static-styles() {
.md3-segmented-button__outline {
border-radius: inherit;
border-style: solid;
// TODO(b/233762888): Move border-width and inset into theme after generating latest version of tokens.
border-width: 1px;
inset: 0px -0.5px;
pointer-events: none;
position: absolute;
z-index: 1;
}
}

View File

@ -0,0 +1,298 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// stylelint-disable selector-class-pattern -- allow `md3-` class selectors.
@use 'sass:map';
@use '@material/web/sass/color';
@use '@material/web/sass/typography';
@use '@material/web/ripple/ripple-theme';
@mixin unselected-ripple($ripple) {
&.md3-segmented-button--unselected {
@include ripple-theme.theme(
(
focus-state-layer-color: map.get($ripple, 'focus-state-layer-color'),
focus-state-layer-opacity: map.get($ripple, 'focus-state-layer-opacity'),
hover-state-layer-color: map.get($ripple, 'hover-state-layer-color'),
hover-state-layer-opacity: map.get($ripple, 'hover-state-layer-opacity'),
pressed-state-layer-color: map.get($ripple, 'pressed-state-layer-color'),
pressed-state-layer-opacity:
map.get($ripple, 'pressed-state-layer-opacity'),
)
);
}
}
@mixin selected-ripple($ripple) {
&.md3-segmented-button--selected {
@include ripple-theme.theme(
(
focus-state-layer-color: map.get($ripple, 'focus-state-layer-color'),
focus-state-layer-opacity: map.get($ripple, 'focus-state-layer-opacity'),
hover-state-layer-color: map.get($ripple, 'hover-state-layer-color'),
hover-state-layer-opacity: map.get($ripple, 'hover-state-layer-opacity'),
pressed-state-layer-color: map.get($ripple, 'pressed-state-layer-color'),
pressed-state-layer-opacity:
map.get($ripple, 'pressed-state-layer-opacity'),
)
);
}
}
///
/// Container color applies the given color to the segmented button container.
/// @param {Color} $color - the color to apply.
///
@mixin container-color($color) {
&.md3-segmented-button--selected:enabled {
background-color: $color;
}
}
///
/// Outline color applies the given color to the segmented button outline.
/// @param {Map} $colors - a map of state keys with color values. Accepted keys
/// include ('default', 'disabled').
///
@mixin outline-colors($colors) {
.md3-segmented-button__outline {
border-color: map.get($colors, 'default');
}
&:disabled .md3-segmented-button__outline {
border-color: map.get($colors, 'disabled');
}
}
///
/// Unselected icon colors sets the color of the icon in the unselected state.
/// @param {Map} $colors - a map of state keys with color values. Accepted keys
/// include ('default', 'hover', 'focus', 'pressed', 'disabled').
///
@mixin unselected-icon-colors($colors) {
@include _icon-color(map.get($colors, 'default'));
&:hover {
@include _icon-color(map.get($colors, 'hover'));
}
&:focus {
@include _icon-color(map.get($colors, 'focus'));
}
&:active {
@include _icon-color(map.get($colors, 'pressed'));
}
&:disabled {
@include _icon-color(map.get($colors, 'disabled'));
}
}
///
/// Selected icon colors sets the color of the icon in the selected state.
/// @param {Map} $colors - a map of state keys with color values. Accepted keys
/// include ('default', 'hover', 'focus', 'pressed', 'disabled').
///
@mixin selected-icon-colors($colors) {
&.md3-segmented-button--selected {
@include _icon-color(map.get($colors, 'default'));
@include _checkmark-color(map.get($colors, 'default'));
&:hover {
@include _checkmark-color(map.get($colors, 'hover'));
}
&:focus {
@include _checkmark-color(map.get($colors, 'focus'));
}
&:active {
@include _checkmark-color(map.get($colors, 'pressed'));
}
&:disabled {
@include _checkmark-color(map.get($colors, 'disabled'));
}
}
}
///
/// Icon size applies the given size to the segmented button icon and checkmark
/// when included.
/// @param {Number} $size - the size of the icon.
///
@mixin icon-size($size) {
.md3-segmented-button__graphic,
.md3-segmented-button__checkmark,
.md3-segmented-button__icon,
.md3-segmented-button__icon ::slotted([slot='icon']) {
height: $size;
width: $size;
font-size: $size;
}
}
///
/// Visible graphic width sets the width of the graphic area when it is visible
/// (i.e. when it has content and should take up space on the screen). The
/// graphic should only take up space under the following conditions:
///
/// 1. The segmented button has an icon and a label.
/// 2. The segmented button is selected and has a label and a checkmark.
/// 3. The segmented button is selected with a checkmark and an icon but has
/// no label.
///
/// @param {Number} $width - the width of the visible graphic.
///
@mixin visible-graphic-width($width) {
&.md3-segmented-button--with-icon.md3-segmented-button--with-label,
&.md3-segmented-button--selected.md3-segmented-button--with-label.md3-segmented-button--with-checkmark,
&.md3-segmented-button--selected.md3-segmented-button--without-label.md3-segmented-button--with-checkmark {
.md3-segmented-button__graphic {
width: $width;
}
}
}
///
/// Checkmark color sets the color of the selected-state checkmark.
/// @param {Map} $colors - a map of state keys with color values. Accepted keys
/// include ('default', 'hover', 'focus', 'pressed', 'disabled').
///
@mixin checkmark-color($colors) {
.md3-segmented-button__checkmark-path {
stroke: $color;
}
}
///
/// Text label font sets the font properties of the text label.
/// @param {Map} $font-map - a map of font keys with font values. Accepted keys
/// include ('font', 'size', 'tracking', weight').
///
@mixin text-label-font($font-map) {
.md3-segmented-button__label-text {
font-family: map.get($font-map, 'font');
font-size: map.get($font-map, 'size');
letter-spacing: map.get($font-map, 'tracking');
font-weight: map.get($font-map, 'weight');
}
}
///
/// Unselected text label color applies the map of state keys and color values
/// to the text label only when the button is both unselected and enabled.
/// @param {Map} $colors - a map of state keys with color values. Accepted keys
/// include ('default', 'hover', 'focus', 'pressed', 'disabled').
///
@mixin unselected-text-label-color($colors) {
&.md3-segmented-button--unselected:enabled {
@include _text-label-colors($colors);
}
&:disabled {
@include _text-label-color(map.get($colors, 'disabled'));
}
}
///
/// Selected text label color applies the map of state keys and color values
/// to the text label only when the button is both selected and enabled.
/// @param {Map} $colors - a map of state keys with color values. Accepted keys
/// include ('default', 'hover', 'focus', 'pressed').
///
@mixin selected-text-label-color($colors) {
&.md3-segmented-button--selected:enabled {
@include _text-label-colors($colors);
}
}
///
/// Icon label color sets the color of the icon.
/// @param {Color} $color - the color of the icon.
///
@mixin _icon-color($color) {
.md3-segmented-button__icon {
color: $color;
}
}
///
/// Checkmark label color sets the color of the checkmark.
/// @param {Color} $color - the color of the checkmark.
///
@mixin _checkmark-color($color) {
.md3-segmented-button__checkmark-path {
stroke: $color;
}
}
///
/// Text label colors applies the given color map to the label text for the
/// configured states.
/// @param {Map} $colors - a map of state keys with color values. Accepted keys
/// include ('default', 'hover', 'focus', 'pressed', 'disabled').
///
@mixin _text-label-colors($colors) {
@include _text-label-color(map.get($colors, 'default'));
&:hover {
@include _text-label-color(map.get($colors, 'hover'));
}
&:focus {
@include _text-label-color(map.get($colors, 'focus'));
}
&:active {
@include _text-label-color(map.get($colors, 'pressed'));
}
}
///
/// Text label color sets the color of the text label.
/// @param {Color} $color - the color of the text label.
///
@mixin _text-label-color($color) {
.md3-segmented-button__label-text {
color: $color;
}
}
///
/// Resolve theme returns the given theme with all values resolved.
///
@function resolve-theme($theme, $resolvers) {
$theme: typography.resolve-theme(
$theme,
map.get($resolvers, 'typography'),
'label-text'
);
$theme: _flatten-disabled-colors($theme);
@return $theme;
}
@function _flatten-disabled-colors($theme) {
@return color.join-color-and-opacity-pairs(
$theme,
(
(
color-key: 'disabled-container-color',
opacity-key: 'disabled-container-opacity'
),
(
color-key: 'disabled-label-text-color',
opacity-key: 'disabled-label-text-opacity'
),
(color-key: 'disabled-icon-color', opacity-key: 'disabled-icon-opacity'),
(
color-key: 'disabled-outline-color',
opacity-key: 'disabled-outline-opacity'
)
)
);
}

View File

@ -0,0 +1,160 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// stylelint-disable selector-class-pattern -- allow `md3-` class selectors.
@use '@material/web/motion/animation';
@use '@material/web/sass/touch-target';
@mixin static-styles() {
@keyframes md3-segmented-button-checkmark-selection-draw-in {
from {
stroke-dashoffset: 29.7833385;
}
to {
stroke-dashoffset: 0;
}
}
@keyframes md3-segmented-button-simple-fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes md3-segmented-button-simple-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
:host {
display: inline-flex;
outline: none;
}
.md3-segmented-button {
align-items: center;
background: transparent;
border: none;
border-radius: inherit;
display: flex;
flex: 1;
justify-content: center;
outline: none;
padding: 0;
position: relative;
vertical-align: middle;
}
.md3-segmented-button:enabled {
cursor: pointer;
}
.md3-segmented-button__focus-ring {
z-index: 1;
}
.md3-segmented-button__ripple {
border-radius: inherit;
z-index: 0;
}
.md3-segmented-button__touch {
@include touch-target.touch-target($width: 100%);
}
.md3-segmented-button__leading,
.md3-segmented-button__graphic {
display: inline-flex;
justify-content: flex-start;
align-items: center;
}
.md3-segmented-button__graphic {
position: relative;
overflow: hidden;
}
.md3-segmented-button__graphic {
transition: animation.standard(width, 150ms);
}
.md3-segmented-button--unselected.md3-segmented-button--with-label,
.md3-segmented-button--unselected.md3-segmented-button--without-label,
.md3-segmented-button--selected.md3-segmented-button--without-checkmark {
.md3-segmented-button__graphic {
width: 0;
}
}
.md3-segmented-button--unselected .md3-segmented-button__checkmark {
opacity: 0;
}
.md3-segmented-button--selected.md3-segmented-button--with-label {
.md3-segmented-button__icon {
opacity: 0;
}
}
.md3-segmented-button--with-label .md3-segmented-button__checkmark {
display: inline-flex;
position: absolute;
}
.md3-segmented-button__checkmark-path {
stroke-width: 2px;
stroke-dasharray: 29.7833385;
}
.md3-segmented-button--selecting {
.md3-segmented-button__checkmark-path {
// We immediately render the checkmark in the animation start treatment
// because we're using an animation delay. If we didn't have the delay,
// the checkmark would render in the base fully-drawn state during the
// brief animation-delay period which would look wrong.
stroke-dashoffset: 29.7833385;
animation: md3-segmented-button-checkmark-selection-draw-in;
animation-duration: 150ms;
animation-delay: 50ms;
animation-fill-mode: forwards;
animation-timing-function: animation.$standard-easing;
}
&.md3-segmented-button--with-label .md3-segmented-button__icon {
animation: md3-segmented-button-simple-fade-out;
animation-duration: 75ms;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
}
.md3-segmented-button--deselecting {
.md3-segmented-button__checkmark {
animation: md3-segmented-button-simple-fade-out;
animation-duration: 50ms;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
&.md3-segmented-button--with-label .md3-segmented-button__icon {
// We immediately render the icon in the animation start treatment
// because we're using an animation delay. If we didn't have the delay,
// the icon would render with full opacity during the brief
// animation-delay period which would look wrong.
opacity: 0;
animation: md3-segmented-button-simple-fade-in;
animation-delay: 50ms;
animation-duration: 150ms;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
}
}

View File

@ -0,0 +1,26 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {html, TemplateResult} from 'lit';
import {ClassInfo} from 'lit/directives/class-map';
import {SegmentedButton} from './segmented-button';
/** @soyCompatible */
export class OutlinedSegmentedButton extends SegmentedButton {
/** @soyTemplate */
protected override getRenderClasses(): ClassInfo {
return {
...super.getRenderClasses(),
'md3-segmented-button--outlined': true,
};
}
/** @soyTemplate */
protected override renderOutline(): TemplateResult {
return html`<span class="md3-segmented-button__outline"></span>`;
}
}

View File

@ -0,0 +1,17 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// stylelint-disable selector-class-pattern -- allow `md3-` class selectors.
@use './outlined-segmented-button';
@use './outlined-segmented-button-theme';
@include outlined-segmented-button.static-styles;
.md3-segmented-button--outlined {
@include outlined-segmented-button-theme.theme-styles(
outlined-segmented-button-theme.$light-theme
);
}

View File

@ -0,0 +1,8 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
@use './segmented-button';
@include segmented-button.static-styles;

View File

@ -0,0 +1,220 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '@material/web/focus/focus-ring';
import {ActionElement, BeginPressConfig, EndPressConfig} from '@material/web/actionelement/action-element';
import {ariaProperty} from '@material/web/decorators/aria-property';
import {pointerPress, shouldShowStrongFocus} from '@material/web/focus/strong-focus';
import {MdRipple} from '@material/web/ripple/ripple';
import {html, PropertyValues, TemplateResult} from 'lit';
import {property, query, queryAssignedElements, state} from 'lit/decorators';
import {ClassInfo, classMap} from 'lit/directives/class-map';
import {ifDefined} from 'lit/directives/if-defined';
/**
* SegmentedButton is a web component implementation of the Material Design
* segmented button component. It is intended **only** for use as a child of a
* `SementedButtonSet` component. It is **not** intended for use in any other
* context.
* @soyCompatible
*/
export class SegmentedButton extends ActionElement {
@property({type: Boolean}) disabled = false;
@property({type: Boolean}) selected = false;
@property({type: String}) label = '';
@property({type: Boolean}) noCheckmark = false;
@property({type: Boolean}) hasIcon = false;
/** @soyPrefixAttribute */
@ariaProperty
@property({type: String, attribute: 'aria-label'})
override ariaLabel!: string;
@state() protected animState: string = '';
@state() protected showFocusRing = false;
@queryAssignedElements({slot: 'icon', flatten: true})
protected iconElement!: HTMLElement[];
@query('md-ripple') ripple!: MdRipple;
protected override update(props: PropertyValues<SegmentedButton>) {
this.animState = this.nextAnimationState(props);
super.update(props);
// NOTE: This needs to be set *after* calling super.update() to ensure the
// appropriate CSS classes are applied.
this.hasIcon = this.iconElement.length > 0;
}
private nextAnimationState(changedProps: PropertyValues<SegmentedButton>):
string {
const prevSelected = changedProps.get('selected');
// Early exit for first update.
if (prevSelected === undefined) return '';
const nextSelected = this.selected;
const nextHasCheckmark = !this.noCheckmark;
if (!prevSelected && nextSelected && nextHasCheckmark) {
return 'selecting';
}
if (prevSelected && !nextSelected && nextHasCheckmark) {
return 'deselecting';
}
return '';
}
override beginPress({positionEvent}: BeginPressConfig) {
this.ripple.beginPress(positionEvent);
}
override endPress(options: EndPressConfig) {
this.ripple.endPress();
super.endPress(options);
if (!options.cancelled) {
const event = new Event(
'segmented-button-interaction', {bubbles: true, composed: true});
this.dispatchEvent(event);
}
}
override handlePointerDown(e: PointerEvent) {
super.handlePointerDown(e);
pointerPress();
this.showFocusRing = shouldShowStrongFocus();
}
override handlePointerUp(e: PointerEvent) {
super.handlePointerUp(e);
}
protected handlePointerEnter(e: PointerEvent) {
this.ripple.beginHover(e);
}
override handlePointerLeave(e: PointerEvent) {
super.handlePointerLeave(e);
this.ripple.endHover();
}
protected handleFocus() {
this.showFocusRing = shouldShowStrongFocus();
}
protected handleBlur() {
this.showFocusRing = false;
}
/** @soyTemplate */
override render(): TemplateResult {
// TODO(b/219531586): Remove clickmod handler once resolved.
return html`
<button
tabindex="${this.disabled ? '-1' : '0'}"
aria-label="${ifDefined(this.ariaLabel)}"
aria-pressed=${this.selected}
.disabled=${this.disabled}
@focus="${this.handleFocus}"
@blur="${this.handleBlur}"
@pointerdown="${this.handlePointerDown}"
@pointerup="${this.handlePointerUp}"
@pointercancel="${this.handlePointerCancel}"
@pointerleave="${this.handlePointerLeave}"
@pointerenter="${this.handlePointerEnter}"
@click="${this.handleClick}"
@clickmod="${this.handleClick}"
@contextmenu="${this.handleContextMenu}"
class="md3-segmented-button ${classMap(this.getRenderClasses())}">
${this.renderFocusRing()}
${this.renderRipple()}
${this.renderOutline()}
${this.renderLeading()}
${this.renderLabel()}
${this.renderTouchTarget()}
</button>
`;
}
/** @soyTemplate */
protected getRenderClasses(): ClassInfo {
return {
'md3-segmented-button--selected': this.selected,
'md3-segmented-button--unselected': !this.selected,
'md3-segmented-button--with-label': this.label !== '',
'md3-segmented-button--without-label': this.label === '',
'md3-segmented-button--with-icon': this.hasIcon,
'md3-segmented-button--with-checkmark': !this.noCheckmark,
'md3-segmented-button--without-checkmark': this.noCheckmark,
'md3-segmented-button--selecting': this.animState === 'selecting',
'md3-segmented-button--deselecting': this.animState === 'deselecting',
};
}
/** @soyTemplate */
protected renderFocusRing(): TemplateResult {
return html`<md-focus-ring .visible="${
this.showFocusRing}" class="md3-segmented-button__focus-ring"></md-focus-ring>`;
}
/** @soyTemplate */
protected renderRipple(): TemplateResult|string {
return html`<md-ripple .disabled="${
this.disabled}" class="md3-segmented-button__ripple"> </md-ripple>`;
}
/** @soyTemplate */
protected renderOutline(): TemplateResult {
return html``;
}
/** @soyTemplate */
protected renderLeading(): TemplateResult {
return this.label === '' ? this.renderLeadingWithoutLabel() :
this.renderLeadingWithLabel();
}
/** @soyTemplate */
protected renderLeadingWithoutLabel(): TemplateResult {
return html`
<span class="md3-segmented-button__leading" aria-hidden="true">
<span class="md3-segmented-button__graphic">
<svg class="md3-segmented-button__checkmark" viewBox="0 0 24 24">
<path class="md3-segmented-button__checkmark-path" fill="none" d="M1.73,12.91 8.1,19.28 22.79,4.59"></path>
</svg>
</span>
<span class="md3-segmented-button__icon" aria-hidden="true">
<slot name="icon"></slot>
</span>
</span>
`;
}
/** @soyTemplate */
protected renderLeadingWithLabel(): TemplateResult {
return html`
<span class="md3-segmented-button__leading" aria-hidden="true">
<span class="md3-segmented-button__graphic">
<svg class="md3-segmented-button__checkmark" viewBox="0 0 24 24">
<path class="md3-segmented-button__checkmark-path" fill="none" d="M1.73,12.91 8.1,19.28 22.79,4.59"></path>
</svg>
<span class="md3-segmented-button__icon" aria-hidden="true">
<slot name="icon"></slot>
</span>
</span>
</span>
`;
}
/** @soyTemplate */
protected renderLabel(): TemplateResult {
return html`
<span class="md3-segmented-button__label-text">${this.label}</span>
`;
}
/** @soyTemplate */
protected renderTouchTarget(): TemplateResult {
return html`<span class="md3-segmented-button__touch"></span>`;
}
}

View File

@ -0,0 +1,29 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {customElement} from 'lit/decorators';
import {OutlinedSegmentedButton} from './lib/outlined-segmented-button';
import {styles as outlinedStyles} from './lib/outlined-styles.css';
import {styles as sharedStyles} from './lib/segmented-button-styles.css';
declare global {
interface HTMLElementTagNameMap {
'md-outlined-segmented-button': MdOutlinedSegmentedButton;
}
}
/**
* MdOutlinedSegmentedButton is the custom element for the Material
* Design outlined segmented button component.
* @soyCompatible
* @final
* @suppress {visibility}
*/
@customElement('md-outlined-segmented-button')
export class MdOutlinedSegmentedButton extends OutlinedSegmentedButton {
static override styles = [sharedStyles, outlinedStyles];
}

View File

@ -0,0 +1,53 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
@use 'sass:map';
@use '@material/web/sass/resolvers';
@use '@material/web/sass/theme';
@use '@material/web/tokens';
@use './segmented-button-set-theme';
$light-theme: tokens.md-comp-outlined-segmented-button-values();
$dark-theme: tokens.md-comp-outlined-segmented-button-values(
(
md-sys-color: tokens.md-sys-color-values-dark-dynamic(),
)
);
@mixin theme($theme, $resolvers: resolvers.$material) {
$theme: theme.validate-theme-styles(
$light-theme,
$theme,
$require-all: false
);
$theme: segmented-button-set-theme.resolve-theme($theme, $resolvers);
$theme: theme.create-theme-vars($theme, 'segmented-button');
@include theme.emit-theme-vars($theme);
}
@mixin theme-styles($theme, $resolvers: resolvers.$material) {
$theme: theme.validate-theme-styles(
$light-theme,
$theme,
$require-all: false
);
$theme: segmented-button-set-theme.resolve-theme($theme, $resolvers);
$theme: theme.create-theme-vars($theme, 'segmented-button');
@include segmented-button-set-theme.container-height(
map.get($theme, 'container-height')
);
@include segmented-button-set-theme.container-shape(
(
'start-start': map.get($theme, 'shape-start-start'),
'end-start': map.get($theme, 'shape-end-start'),
'start-end': map.get($theme, 'shape-start-end'),
'end-end': map.get($theme, 'shape-end-end'),
)
);
}

View File

@ -0,0 +1,12 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// stylelint-disable selector-class-pattern -- allow `md3-` class selectors.
@mixin static-styles() {
.md3-segmented-button-set--outlined {
// TODO(b/213634341): Write styles.
}
}

View File

@ -0,0 +1,41 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
@use 'sass:map';
@use '@material/web/sass/shape';
///
/// Container height sets the height of the container.
/// @param {Number} $height - the height of the container.
///
@mixin container-height($height) {
height: $height;
}
///
/// Container shape sets the shape of the container.
/// @param {Map} $shape - a map of CSS logical radius keys with number values.
/// Accepted keys include ('start-start', 'end-start', 'start-end',
/// 'end-end').
///
@mixin container-shape($shape) {
::slotted(:first-child) {
border-start-start-radius: map.get($shape, 'start-start');
border-end-start-radius: map.get($shape, 'end-start');
}
::slotted(:last-child) {
border-start-end-radius: map.get($shape, 'start-end');
border-end-end-radius: map.get($shape, 'end-end');
}
}
///
/// Resolve theme returns the given theme with all values resolved.
///
@function resolve-theme($theme, $resolvers) {
$theme: shape.resolve-theme($theme, map.get($resolvers, 'shape'), 'shape');
@return $theme;
}

View File

@ -0,0 +1,21 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// stylelint-disable selector-class-pattern -- allow `md3-` class selectors.
@mixin static-styles() {
:host {
display: flex;
outline: none;
}
.md3-segmented-button-set {
display: grid;
grid-auto-columns: 1fr;
grid-auto-flow: column;
grid-auto-rows: auto;
width: 100%;
}
}

View File

@ -0,0 +1,20 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {ClassInfo} from 'lit/directives/class-map';
import {SegmentedButtonSet} from './segmented-button-set';
/** @soyCompatible */
export class OutlinedSegmentedButtonSet extends SegmentedButtonSet {
/** @soyTemplate */
protected override getRenderClasses(): ClassInfo {
return {
...super.getRenderClasses(),
'md3-segmented-button-set--outlined': true,
};
}
}

View File

@ -0,0 +1,17 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// stylelint-disable selector-class-pattern -- allow `md3-` class selectors.
@use './outlined-segmented-button-set';
@use './outlined-segmented-button-set-theme';
@include outlined-segmented-button-set.static-styles;
.md3-segmented-button-set--outlined {
@include outlined-segmented-button-set-theme.theme-styles(
outlined-segmented-button-set-theme.$light-theme
);
}

View File

@ -0,0 +1,77 @@
/**
* @requirecss {segmented_button_set.lib.shared_styles}
*
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {ariaProperty} from '@material/web/decorators/aria-property';
import {html, LitElement, TemplateResult} from 'lit';
import {property, queryAssignedElements} from 'lit/decorators';
import {ClassInfo, classMap} from 'lit/directives/class-map';
import {ifDefined} from 'lit/directives/if-defined';
import {SegmentedButton} from '../../segmented_button/lib/segmented-button';
/**
* SegmentedButtonSet is the parent component for two or more
* `SegmentedButton` components. **Only** `SegmentedButton` components may be
* used as children.
* @soyCompatible
*/
export class SegmentedButtonSet extends LitElement {
@property({type: Boolean}) multiselect = false;
/** @soyPrefixAttribute */
@ariaProperty
@property({type: String, attribute: 'aria-label'})
override ariaLabel!: string;
@queryAssignedElements({flatten: true}) buttons!: SegmentedButton[];
private handleSegmentedButtonInteraction(e: CustomEvent) {
// TODO(b/229912696): Support interactions.
const index = this.buttons.indexOf(e.target as SegmentedButton);
this.toggleSelection(index);
}
private toggleSelection(index: number) {
if (this.indexOutOfBounds(index)) return;
if (this.multiselect) {
this.buttons[index].selected = !this.buttons[index].selected;
return;
}
// Single-select segmented buttons are not unselectable.
this.buttons[index].selected = true;
// Deselect all other buttons for single-select.
for (let i = 0; i < this.buttons.length; i++) {
if (i === index) continue;
this.buttons[i].selected = false;
}
}
private indexOutOfBounds(index: number): boolean {
return index < 0 || index >= this.buttons.length;
}
/** @soyTemplate */
override render(): TemplateResult {
return html`
<span
role="group"
@segmented-button-interaction="${this.handleSegmentedButtonInteraction}"
aria-label="${ifDefined(this.ariaLabel)}"
class="md3-segmented-button-set ${classMap(this.getRenderClasses())}">
<slot></slot>
</span>
`;
}
/** @soyTemplate */
protected getRenderClasses(): ClassInfo {
return {
// TODO(b/213634341): Write styles.
};
}
}

View File

@ -0,0 +1,8 @@
//
// Copyright 2022 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
@use './segmented-button-set';
@include segmented-button-set.static-styles;

View File

@ -0,0 +1,29 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {customElement} from 'lit/decorators';
import {OutlinedSegmentedButtonSet} from './lib/outlined-segmented-button-set';
import {styles as outlinedStyles} from './lib/outlined-styles.css';
import {styles as sharedStyles} from './lib/shared-styles.css';
declare global {
interface HTMLElementTagNameMap {
'md-outlined-segmented-button-set': MdOutlinedSegmentedButtonSet;
}
}
/**
* MdOutlinedSegmentedButtonSet is the custom element for the Material
* Design outlined segmented button set component.
* @soyCompatible
* @final
* @suppress {visibility}
*/
@customElement('md-outlined-segmented-button-set')
export class MdOutlinedSegmentedButtonSet extends OutlinedSegmentedButtonSet {
static override styles = [sharedStyles, outlinedStyles];
}