mirror of
https://github.com/material-components/material-web.git
synced 2026-01-09 07:21:09 +08:00
fix(tabs): incorrect layout and primary indicator width
PiperOrigin-RevId: 561091366
This commit is contained in:
parent
5b13b5c05b
commit
0467c4845d
@ -57,13 +57,8 @@
|
||||
--md-secondary-tab-container-shape-end-start,
|
||||
var(--_container-shape)
|
||||
);
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
min-width: 100%;
|
||||
// TODO(b/293503309): remove once secondary tabs only use inline icons
|
||||
--_with-icon-and-label-text-container-height: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,7 +57,7 @@
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
padding: 0 16px;
|
||||
margin: 0;
|
||||
z-index: 0; // Ensure this is a stacking context so the indicator displays
|
||||
font: var(--_label-text-type);
|
||||
@ -92,18 +92,17 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: var(--_container-height);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
$_content-padding: 8px;
|
||||
// tabs are naturally sized up to their max height.
|
||||
max-height: calc(var(--_container-height) + 2 * $_content-padding);
|
||||
// min-height of touch target
|
||||
min-height: 48px;
|
||||
padding: $_content-padding calc(2 * $_content-padding);
|
||||
gap: 4px;
|
||||
.content.has-icon.has-label:not(.inline-icon) {
|
||||
height: var(--_with-icon-and-label-text-container-height);
|
||||
}
|
||||
|
||||
.content.inline-icon {
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
|
||||
@ -9,4 +9,6 @@ import {Tab} from './tab.js';
|
||||
/**
|
||||
* A secondary tab component.
|
||||
*/
|
||||
export class SecondaryTab extends Tab {}
|
||||
export class SecondaryTab extends Tab {
|
||||
protected override fullWidthIndicator = true;
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import '../../focus/md-focus-ring.js';
|
||||
import '../../ripple/ripple.js';
|
||||
|
||||
import {html, isServer, LitElement, nothing, PropertyValues} from 'lit';
|
||||
import {property, query} from 'lit/decorators.js';
|
||||
import {property, query, queryAssignedElements, queryAssignedNodes, state} from 'lit/decorators.js';
|
||||
import {classMap} from 'lit/directives/class-map.js';
|
||||
|
||||
import {ARIAMixinStrict} from '../../internal/aria/aria.js';
|
||||
@ -56,13 +56,26 @@ export class Tab extends LitElement {
|
||||
*/
|
||||
@property({type: Boolean, attribute: 'inline-icon'}) inlineIcon = false;
|
||||
|
||||
/**
|
||||
* In SSR, set this to true when an icon is present.
|
||||
*/
|
||||
@property({type: Boolean, attribute: 'has-icon'}) hasIcon = false;
|
||||
|
||||
/**
|
||||
* In SSR, set this to true when there is no label and only an icon.
|
||||
*/
|
||||
@property({type: Boolean, attribute: 'icon-only'}) iconOnly = false;
|
||||
|
||||
@query('.button') private readonly button!: HTMLElement|null;
|
||||
|
||||
// note, this is public so it can participate in selection animation.
|
||||
/**
|
||||
* Selection indicator element.
|
||||
*/
|
||||
/** @private */
|
||||
@query('.indicator') readonly indicator!: HTMLElement;
|
||||
@state() protected fullWidthIndicator = false;
|
||||
@queryAssignedNodes({flatten: true})
|
||||
private readonly assignedDefaultNodes!: Node[];
|
||||
@queryAssignedElements({slot: 'icon', flatten: true})
|
||||
private readonly assignedIcons!: HTMLElement[];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@ -82,7 +95,11 @@ export class Tab extends LitElement {
|
||||
protected override render() {
|
||||
const contentClasses = {
|
||||
'inline-icon': this.inlineIcon,
|
||||
'has-icon': this.hasIcon,
|
||||
'has-label': !this.iconOnly,
|
||||
};
|
||||
|
||||
const indicator = html`<div class="indicator"></div>`;
|
||||
// Needed for closure conformance
|
||||
const {ariaLabel} = this as ARIAMixinStrict;
|
||||
return html`
|
||||
@ -97,12 +114,11 @@ export class Tab extends LitElement {
|
||||
<md-elevation></md-elevation>
|
||||
<md-ripple></md-ripple>
|
||||
<div class="content ${classMap(contentClasses)}">
|
||||
<slot name="icon"></slot>
|
||||
<span class="label">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<div class="indicator"></div>
|
||||
<slot name="icon" @slotchange=${this.handleIconSlotChange}></slot>
|
||||
<slot @slotchange=${this.handleSlotChange}></slot>
|
||||
${this.fullWidthIndicator ? nothing : indicator}
|
||||
</div>
|
||||
${this.fullWidthIndicator ? indicator : nothing}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
@ -161,6 +177,25 @@ export class Tab extends LitElement {
|
||||
// that can hide the animation.
|
||||
return [from, {'transform': 'none'}];
|
||||
}
|
||||
|
||||
private handleSlotChange() {
|
||||
this.iconOnly = false;
|
||||
// Check if there's any label text or elements. If not, then there is only
|
||||
// an icon.
|
||||
for (const node of this.assignedDefaultNodes) {
|
||||
const hasTextContent = node.nodeType === Node.TEXT_NODE &&
|
||||
!!(node as Text).wholeText.match(/\S/);
|
||||
if (node.nodeType === Node.ELEMENT_NODE || hasTextContent) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.iconOnly = true;
|
||||
}
|
||||
|
||||
private handleIconSlotChange() {
|
||||
this.hasIcon = this.assignedIcons.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldReduceMotion() {
|
||||
|
||||
@ -59,13 +59,11 @@ $supported-tokens: (
|
||||
'pressed-label-text-color',
|
||||
'pressed-state-layer-color',
|
||||
'pressed-state-layer-opacity',
|
||||
'with-icon-and-label-text-container-height',
|
||||
// go/keep-sorted end
|
||||
);
|
||||
|
||||
$unsupported-tokens: (
|
||||
// include an icon and the size will adjust;
|
||||
// height is 48 and it's 64 with icon
|
||||
'with-icon-and-label-text-container-height',
|
||||
'with-label-text-label-text-font',
|
||||
'with-label-text-label-text-line-height',
|
||||
'with-label-text-label-text-size',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user