ember-bootstrap
Version:
Bootstrap components for Ember.js
227 lines (201 loc) • 5.71 kB
text/typescript
import { action } from '@ember/object';
import Component from '@glimmer/component';
import arg from 'ember-bootstrap/utils/decorators/arg';
import { tracked } from '@glimmer/tracking';
import { getOwnConfig, macroCondition } from '@embroider/macros';
import { trackedRef } from 'ember-ref-bucket';
import type { EmberBootstrapMacrosConfig } from '../../macros-config';
import type {
Placement,
Options as PopperOptions,
Boundary,
Padding,
ModifierArguments,
State,
} from '@popperjs/core';
import type { ArrowModifier } from '@popperjs/core/lib/modifiers/arrow';
import type { PreventOverflowModifier } from '@popperjs/core/lib/modifiers/preventOverflow';
import type { FlipModifier } from '@popperjs/core/lib/modifiers/flip';
export interface ContextualHelpElementSignature {
Args: {
autoPlacement?: boolean;
placement?: Placement;
viewportElement?: Boundary;
viewportPadding?: Padding;
};
}
/**
Internal (abstract) component for contextual help markup. Should not be used directly.
@class ContextualHelpElement
@namespace Components
@extends Glimmer.Component
@private
*/
export default class ContextualHelpElement<
Signature extends ContextualHelpElementSignature,
> extends Component<Signature> {
/**
* @property placement
* @type string
* @default 'top'
* @public
*/
placement: Placement = 'top';
// `actualPlacement` is initialized with `@placement` and updated by Popper
// through the `updatePlacement` method which is bound to Popper's
// `onFirstUpdate` hook.
//
// The options passed to Popper are autotracked. And Ember Popper Modifier
// updates Popper with the new options whenever they change.
//
// `actualPlacement` is not updated when `@placement` changes and Popper's
// `onFirstUpdate` hook does not run. It is unclear if that situation can
// ever happen. But in case we get a bug report related to layout issues
// when `@placement` argument changed after initial rendering, it may be
// related.
//
// eslint-disable-next-line ember/no-tracked-properties-from-args
actualPlacement?: Placement = this.args.placement;
/**
* @property fade
* @type boolean
* @default true
* @public
*/
fade = true;
/**
* @property showHelp
* @type boolean
* @default false
* @public
*/
showHelp = false;
/**
* If true component will render in place, rather than be wormholed.
*
* @property renderInPlace
* @type boolean
* @default true
* @public
*/
/**
* Which element to align to
*
* @property popperTarget
* @type {string|HTMLElement}
* @public
*/
/**
* @property autoPlacement
* @type boolean
* @default true
* @public
*/
/**
* The DOM element of the viewport element.
*
* @property viewportElement
* @type object
* @public
*/
/**
* Take a padding into account for keeping the tooltip/popover within the bounds of the element given by `viewportElement`.
*
* @property viewportPadding
* @type number
* @default 0
* @public
*/
/**
* @property arrowClass
* @private
*/
arrowClass = 'arrow';
placementClassPrefix = '';
offset = [0, 0];
declare popperElement: HTMLElement;
/**
* popper.js modifier config
*
* @property popperModifiers
* @type {object}
* @private
*/
get popperOptions() {
const options: PopperOptions = {
placement: this.placement,
// Popper's `onFirstUpdate` hook only runs when Popper positioned the element
// the first time. But not when it updates that position later. This may lead
// to `actualPlacement` property getting out of sync. It is unclear if this
// leads to any actual bugs. But we should investigate it as a potential root
// cause when we get a bug report related to placement of popovers or tooltips.
onFirstUpdate: this.updatePlacement,
modifiers: [],
strategy: 'absolute',
};
// We need popperElement, so we wait for this getter to recompute once it's available
if (!this.popperElement) {
return options;
}
options.modifiers = [
{
name: 'arrow',
options: {
element: this.popperElement.querySelector(`.${this.arrowClass}`),
padding: 4,
},
} as ArrowModifier,
{
name: 'offset',
options: {
offset: this.offset,
},
},
{
name: 'preventOverflow',
enabled: this.args.autoPlacement,
options: {
boundary: this.args.viewportElement,
padding: this.args.viewportPadding,
},
} as PreventOverflowModifier,
{
name: 'flip',
enabled: this.args.autoPlacement,
} as FlipModifier,
{
name: 'onChange',
enabled: true,
phase: 'afterWrite',
fn: this.updatePlacement,
},
];
return options;
}
get actualPlacementClass() {
let ending: string | undefined = this.actualPlacement;
if (macroCondition(getOwnConfig<EmberBootstrapMacrosConfig>().isBS5)) {
if (ending === 'right') {
ending = 'end';
}
if (ending === 'left') {
ending = 'start';
}
}
return this.placementClassPrefix + ending;
}
updatePlacement(state: Partial<State> | ModifierArguments<object>) {
// normalize argument
const normalizedState: Partial<State> | ModifierArguments<object> =
'state' in state ? state.state : state;
if (this.actualPlacement === normalizedState.placement) {
return;
}
this.actualPlacement = normalizedState.placement;
}
}