refactor(menu,select)!: rename fixed to positioning

This will enable forwards compatibility for `positioning="top-layer"` with popover.

BREAKING CHANGE: refactor `fixed` property to `positioning="fixed"` in Menu and `menuFixed` to `menuPositioning="fixed"`

PiperOrigin-RevId: 567723646
This commit is contained in:
Elliott Marquez 2023-09-22 14:53:32 -07:00 committed by Copybara-Service
parent 2a1d8776a7
commit 63b01425e7
14 changed files with 101 additions and 71 deletions

View File

@ -398,7 +398,7 @@ export function selectDropdown<T extends string>({
<label>
<md-filled-select
@change="${valueChanged}"
menu-fixed
menu-positioning="fixed"
style=${styleMap(sharedTextFieldStyles)}>
${listItems}
</md-filled-select>

View File

@ -195,7 +195,7 @@
overflow: hidden;
// needed to display scrollbars on Chrome linux. Also needs to be > 0 so
// that content that is position: fixed in the content can render above the
// actions bar. e.g. <md-select menu-fixed>
// actions bar. e.g. <md-select positioning="menu-fixed">
z-index: 1;
}

View File

@ -7,7 +7,7 @@
<div style="margin: 16px">
<md-filled-button id="usage-fixed-anchor">Open fixed menu</md-filled-button>
</div>
<md-menu fixed id="usage-fixed" anchor="usage-fixed-anchor">
<md-menu positioning="fixed" id="usage-fixed" anchor="usage-fixed-anchor">
<md-menu-item headline="Apple"></md-menu-item>
<md-menu-item headline="Banana"></md-menu-item>
<md-menu-item headline="Cucumber"></md-menu-item>

View File

@ -194,8 +194,8 @@ Internally menu uses `position: absolute` by default. Though there are cases
when the anchor and the node cannot share a common ancestor that is `position:
relative`, or sometimes, menu will render below another item due to limitations
with `position: absolute`. In most of these cases, you would want to use the
`fixed` attribute to position the menu relative to the window instead of
relative to the parent.
`positioning="fixed"` attribute to position the menu relative to the window
instead of relative to the parent.
> Note: Fixed menu positions are positioned relative to the window and not the
> document. This means that the menu will not scroll with the anchor as the page
@ -217,7 +217,7 @@ Cucumber."](images/menu/usage-fixed.webp)
</div>
<!-- Fixed menus do not require a common ancestor with the anchor. -->
<md-menu fixed id="usage-fixed" anchor="usage-fixed-anchor">
<md-menu positioning="fixed" id="usage-fixed" anchor="usage-fixed-anchor">
<md-menu-item headline="Apple"></md-menu-item>
<md-menu-item headline="Banana"></md-menu-item>
<md-menu-item headline="Cucumber"></md-menu-item>
@ -353,7 +353,6 @@ a sharp 0px border radius.](images/menu/theming.webp)
## API
### MdMenu &lt;md-menu&gt;
#### Properties

View File

@ -49,11 +49,16 @@ const collection =
]
}),
}),
new Knob('open', {
defaultValue: false,
ui: boolInput(),
new Knob('positioning', {
defaultValue: 'absolute' as const,
ui: selectDropdown<'absolute'|'fixed'>({
options: [
{label: 'absolute', value: 'absolute'},
{label: 'fixed', value: 'fixed'},
]
})
}),
new Knob('fixed', {
new Knob('open', {
defaultValue: false,
ui: boolInput(),
}),

View File

@ -23,8 +23,8 @@ export interface StoryKnobs {
anchorCorner: Corner|undefined;
menuCorner: Corner|undefined;
defaultFocus: FocusState|undefined;
positioning: 'absolute'|'fixed'|undefined;
open: boolean;
fixed: boolean;
quick: boolean;
hasOverflow: boolean;
stayOpenOnOutsideClick: boolean;
@ -120,7 +120,7 @@ const standard: MaterialStoryInit<StoryKnobs> = {
.menuCorner="${knobs.menuCorner!}"
.xOffset=${knobs.xOffset}
.yOffset=${knobs.yOffset}
.fixed=${knobs.fixed}
.positioning=${knobs.positioning!}
.defaultFocus=${knobs.defaultFocus!}
.skipRestoreFocus=${knobs.skipRestoreFocus}
.typeaheadDelay=${knobs.typeaheadDelay}
@ -188,7 +188,7 @@ const linkable: MaterialStoryInit<StoryKnobs> = {
.menuCorner="${knobs.menuCorner!}"
.xOffset=${knobs.xOffset}
.yOffset=${knobs.yOffset}
.fixed=${knobs.fixed}
.positioning=${knobs.positioning!}
.defaultFocus=${knobs.defaultFocus!}
.skipRestoreFocus=${knobs.skipRestoreFocus}
.typeaheadDelay=${knobs.typeaheadDelay}
@ -250,7 +250,7 @@ const submenu: MaterialStoryInit<StoryKnobs> = {
.ariaLabel=${knobs.ariaLabel}
.xOffset=${knobs.xOffset}
.yOffset=${knobs.yOffset}
.fixed=${knobs.fixed}
.positioning=${knobs.positioning!}
.defaultFocus=${knobs.defaultFocus!}
.typeaheadDelay=${knobs.typeaheadDelay}>
${layer2}
@ -296,7 +296,7 @@ const submenu: MaterialStoryInit<StoryKnobs> = {
.ariaLabel=${knobs.ariaLabel}
.xOffset=${knobs.xOffset}
.yOffset=${knobs.yOffset}
.fixed=${knobs.fixed}
.positioning=${knobs.positioning!}
.defaultFocus=${knobs.defaultFocus!}
.typeaheadDelay=${knobs.typeaheadDelay}>
${layer1}
@ -327,7 +327,7 @@ const submenu: MaterialStoryInit<StoryKnobs> = {
.menuCorner="${knobs.menuCorner!}"
.xOffset=${knobs.xOffset}
.yOffset=${knobs.yOffset}
.fixed=${knobs.fixed}
.positioning=${knobs.positioning!}
.defaultFocus=${knobs.defaultFocus!}
.skipRestoreFocus=${knobs.skipRestoreFocus}
.typeaheadDelay=${knobs.typeaheadDelay}
@ -377,7 +377,7 @@ const menuWithoutButton: MaterialStoryInit<StoryKnobs> = {
.menuCorner="${knobs.menuCorner!}"
.xOffset=${knobs.xOffset}
.yOffset=${knobs.yOffset}
.fixed=${knobs.fixed}
.positioning=${knobs.positioning!}
.defaultFocus=${knobs.defaultFocus!}
.skipRestoreFocus=${knobs.skipRestoreFocus}
.typeaheadDelay=${knobs.typeaheadDelay}

View File

@ -17,7 +17,7 @@ import {createAnimationSignal, EASING} from '../../internal/motion/animation.js'
import {ListController, NavigableKeys} from './list-controller.js';
import {getActiveItem, getFirstActivatableItem, getLastActivatableItem} from './list-navigation-helpers.js';
import {ActivateTypeaheadEvent, DeactivateTypeaheadEvent, isClosableKey, isElementInSubtree, MenuItem} from './shared.js';
import {ActivateTypeaheadEvent, DeactivateTypeaheadEvent, FocusState, isClosableKey, isElementInSubtree, MenuItem} from './shared.js';
import {Corner, SurfacePositionController, SurfacePositionTarget} from './surfacePositionController.js';
import {TypeaheadController} from './typeaheadController.js';
@ -41,22 +41,6 @@ const menuNavKeys = new Set<string>([
...submenuNavKeys,
]);
/**
* Element to focus on when menu is first opened.
*/
// tslint:disable-next-line:enforce-name-casing We are mimicking enum style
export const FocusState = {
NONE: 'none',
LIST_ROOT: 'list-root',
FIRST_ITEM: 'first-item',
LAST_ITEM: 'last-item'
} as const;
/**
* Element to focus on when menu is first opened.
*/
export type FocusState = typeof FocusState[keyof typeof FocusState];
/**
* Gets the currently focused element on the page.
*
@ -101,15 +85,26 @@ export abstract class Menu extends LitElement {
*/
@property() anchor = '';
/**
* Makes the element use `position:fixed` instead of `position:absolute`. In
* most cases, the menu should position itself above most other
* `position:absolute` or `position:fixed` elements when placed inside of
* them. e.g. using a menu inside of an `md-dialog`.
* Whether the positioning algorithim should calculate relative to the parent
* of the anchor element (absolute) or relative to the window (fixed).
*
* Examples for `position = 'fixed'`:
*
* - If there is no `position:relative` in the given parent tree and the
* surface is `position:absolute`
* - If the surface is `position:fixed`
* - If the surface is in the "top layer"
* - The anchor and the surface do not share a common `position:relative`
* ancestor
*
* When using positioning = fixed, in most cases, the menu should position
* itself above most other `position:absolute` or `position:fixed` elements
* when placed inside of them. e.g. using a menu inside of an `md-dialog`.
*
* __NOTE__: Fixed menus will not scroll with the page and will be fixed to
* the window instead.
*/
@property({type: Boolean}) fixed = false;
@property() positioning: 'absolute'|'fixed' = 'absolute';
/**
* Skips the opening and closing animations.
*/
@ -325,7 +320,7 @@ export abstract class Menu extends LitElement {
surfaceCorner: this.menuCorner,
surfaceEl: this.surfaceEl,
anchorEl: this.anchorElement,
isTopLayer: this.fixed,
positioning: this.positioning,
isOpen: this.open,
xOffset: this.xOffset,
yOffset: this.yOffset,
@ -403,7 +398,7 @@ export abstract class Menu extends LitElement {
private getSurfaceClasses() {
return {
open: this.open,
fixed: this.fixed,
fixed: this.positioning === 'fixed',
'has-overflow': this.hasOverflow,
};
}

View File

@ -227,3 +227,19 @@ export function isElementInSubtree(
const isContained = composedPath.length > 0;
return isContained;
}
/**
* Element to focus on when menu is first opened.
*/
// tslint:disable-next-line:enforce-name-casing We are mimicking enum style
export const FocusState = {
NONE: 'none',
LIST_ROOT: 'list-root',
FIRST_ITEM: 'first-item',
LAST_ITEM: 'last-item'
} as const;
/**
* Element to focus on when menu is first opened.
*/
export type FocusState = typeof FocusState[keyof typeof FocusState];

View File

@ -53,10 +53,10 @@ export interface SurfacePositionControllerProperties {
*/
anchorEl: SurfacePositionTarget|null;
/**
* Whether or not the calculation should be relative to the top layer rather
* than relative to the parent of the anchor.
* Whether the positioning algorithim should calculate relative to the parent
* of the anchor element (absolute) or relative to the window (fixed).
*
* Examples for `isTopLayer:true`:
* Examples for `position = 'fixed'`:
*
* - If there is no `position:relative` in the given parent tree and the
* surface is `position:absolute`
@ -65,7 +65,7 @@ export interface SurfacePositionControllerProperties {
* - The anchor and the surface do not share a common `position:relative`
* ancestor
*/
isTopLayer: boolean;
positioning: 'absolute'|'fixed';
/**
* Whether or not the surface should be "open" and visible
*/
@ -153,7 +153,7 @@ export class SurfacePositionController implements ReactiveController {
anchorEl,
anchorCorner: anchorCornerRaw,
surfaceCorner: surfaceCornerRaw,
isTopLayer,
positioning,
xOffset,
yOffset,
repositionStrategy,
@ -231,7 +231,7 @@ export class SurfacePositionController implements ReactiveController {
anchorBlock,
surfaceBlock,
yOffset,
isTopLayer
positioning
});
// If the surface should be out of bounds in the block direction, flip the
@ -246,7 +246,7 @@ export class SurfacePositionController implements ReactiveController {
anchorBlock: flippedAnchorBlock,
surfaceBlock: flippedSurfaceBlock,
yOffset,
isTopLayer
positioning
});
// In the case that the flipped verion would require less out of bounds
@ -267,7 +267,7 @@ export class SurfacePositionController implements ReactiveController {
anchorInline,
surfaceInline,
xOffset,
isTopLayer,
positioning,
isLTR,
});
@ -283,7 +283,7 @@ export class SurfacePositionController implements ReactiveController {
anchorInline: flippedAnchorInline,
surfaceInline: flippedSurfaceInline,
xOffset,
isTopLayer,
positioning,
isLTR,
});
@ -340,7 +340,7 @@ export class SurfacePositionController implements ReactiveController {
anchorBlock: 'start'|'end',
surfaceBlock: 'start'|'end',
yOffset: number,
isTopLayer: boolean,
positioning: 'absolute'|'fixed',
}) {
const {
surfaceRect,
@ -348,11 +348,11 @@ export class SurfacePositionController implements ReactiveController {
anchorBlock,
surfaceBlock,
yOffset,
isTopLayer: isTopLayerBool,
positioning,
} = config;
// We use number booleans to multiply values rather than `if` / ternary
// statements because it _heavily_ cuts down on nesting and readability
const isTopLayer = isTopLayerBool ? 1 : 0;
const relativeToWindow = positioning === 'fixed' ? 1 : 0;
const isSurfaceBlockStart = surfaceBlock === 'start' ? 1 : 0;
const isSurfaceBlockEnd = surfaceBlock === 'end' ? 1 : 0;
const isOneBlockEnd = anchorBlock !== surfaceBlock ? 1 : 0;
@ -371,7 +371,8 @@ export class SurfacePositionController implements ReactiveController {
// The block logical value of the surface
const blockInset = isTopLayer * blockTopLayerOffset + blockAnchorOffset;
const blockInset =
relativeToWindow * blockTopLayerOffset + blockAnchorOffset;
const surfaceBlockProperty =
surfaceBlock === 'start' ? 'inset-block-start' : 'inset-block-end';
@ -390,7 +391,7 @@ export class SurfacePositionController implements ReactiveController {
anchorRect: DOMRect,
surfaceRect: DOMRect,
xOffset: number,
isTopLayer: boolean,
positioning: 'absolute'|'fixed',
}) {
const {
isLTR: isLTRBool,
@ -399,11 +400,11 @@ export class SurfacePositionController implements ReactiveController {
anchorRect,
surfaceRect,
xOffset,
isTopLayer: isTopLayerBool,
positioning,
} = config;
// We use number booleans to multiply values rather than `if` / ternary
// statements because it _heavily_ cuts down on nesting and readability
const isTopLayer = isTopLayerBool ? 1 : 0;
const relativeToWindow = positioning === 'fixed' ? 1 : 0;
const isLTR = isLTRBool ? 1 : 0;
const isRTL = isLTRBool ? 0 : 1;
const isSurfaceInlineStart = surfaceInline === 'start' ? 1 : 0;
@ -432,7 +433,8 @@ export class SurfacePositionController implements ReactiveController {
// The inline logical value of the surface
const inlineInset = isTopLayer * inlineTopLayerOffset + inlineAnchorOffset;
const inlineInset =
relativeToWindow * inlineTopLayerOffset + inlineAnchorOffset;
const surfaceInlineProperty =
surfaceInline === 'start' ? 'inset-inline-start' : 'inset-inline-end';

View File

@ -11,8 +11,8 @@ import {Menu} from './internal/menu.js';
import {styles} from './internal/menu-styles.css.js';
export {ListItem} from '../list/internal/listitem/list-item.js';
export {Corner, FocusState} from './internal/menu.js';
export {CloseMenuEvent, MenuItem} from './internal/shared.js';
export {Corner} from './internal/menu.js';
export {CloseMenuEvent, FocusState, MenuItem} from './internal/shared.js';
declare global {
interface HTMLElementTagNameMap {

View File

@ -28,8 +28,9 @@ declare global {
* `deselect-items` events.
*
* This menu item will open a sub-menu that is slotted in the `submenu` slot.
* Additionally, the containing menu must either have `has-overflow` or `fixed`
* set to `true` in order to display the containing menu properly.
* Additionally, the containing menu must either have `has-overflow` or
* `positioning=fixed` set to `true` in order to display the containing menu
* properly.
*
* @example
* ```html

View File

@ -8,7 +8,7 @@ import './index.js';
import './material-collection.js';
import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo, title} from './material-collection.js';
import {boolInput, Knob, numberInput, textInput} from './index.js';
import {boolInput, Knob, numberInput, selectDropdown, textInput} from './index.js';
import {stories, StoryKnobs} from './stories.js';
@ -22,8 +22,16 @@ const collection =
new Knob('disabled', {ui: boolInput(), defaultValue: false}),
new Knob('errorText', {ui: textInput(), defaultValue: ''}),
new Knob('supportingText', {ui: textInput(), defaultValue: ''}),
new Knob('menuPositioning', {
defaultValue: 'absolute' as const,
ui: selectDropdown<'absolute'|'fixed'>({
options: [
{label: 'absolute', value: 'absolute'},
{label: 'fixed', value: 'fixed'},
]
})
}),
new Knob('error', {ui: boolInput(), defaultValue: false}),
new Knob('menuFixed', {ui: boolInput(), defaultValue: false}),
new Knob('md-select Slots', {ui: title()}),
new Knob('slot=leading-icon', {ui: textInput(), defaultValue: ''}),

View File

@ -23,7 +23,7 @@ export interface StoryKnobs {
errorText: string;
supportingText: string;
error: boolean;
menuFixed: boolean;
menuPositioning: 'absolute'|'fixed'|undefined;
'md-select Slots': void;
'slot=leading-icon': string;
@ -41,7 +41,7 @@ const outlined: MaterialStoryInit<StoryKnobs> = {
.disabled=${knobs.disabled}
.errorText=${knobs.errorText}
.supportingText=${knobs.supportingText}
.menuFixed=${knobs.menuFixed}
.menuPositioning=${knobs.menuPositioning!}
.typeaheadDelay=${knobs.typeaheadDelay}
.error=${knobs.error}>
${renderIcon(knobs['slot=leading-icon'], 'leading-icon')}
@ -63,7 +63,7 @@ const filled: MaterialStoryInit<StoryKnobs> = {
.disabled=${knobs.disabled}
.errorText=${knobs.errorText}
.supportingText=${knobs.supportingText}
.menuFixed=${knobs.menuFixed}
.menuPositioning=${knobs.menuPositioning!}
.typeaheadDelay=${knobs.typeaheadDelay}
.error=${knobs.error}>
${renderIcon(knobs['slot=leading-icon'], 'leading-icon')}

View File

@ -77,9 +77,13 @@ export abstract class Select extends LitElement {
@property({type: Boolean, reflect: true}) error = false;
/**
* Whether or not the underlying md-menu should be position: fixed to display
* in a top-level manner.
* in a top-level manner, or position: absolute.
*
* position:fixed is useful for cases where select is inside of another
* element with stacking context and hidden overflows such as `md-dialog`.
*/
@property({type: Boolean, attribute: 'menu-fixed'}) menuFixed = false;
@property({attribute: 'menu-positioning'})
menuPositioning: 'absolute'|'fixed' = 'absolute';
/**
* The max time between the keystrokes of the typeahead select / menu behavior
* before it clears the typeahead buffer.
@ -478,7 +482,7 @@ export abstract class Select extends LitElement {
anchor="field"
.open=${this.open}
.quick=${this.quick}
.fixed=${this.menuFixed}
.positioning=${this.menuPositioning}
.typeaheadDelay=${this.typeaheadDelay}
@opening=${this.handleOpening}
@opened=${this.redispatchEvent}