@trimble-oss/moduswebcomponents
Version:
Modus Web Components is a modern, accessible UI library built with Stencil JS that provides reusable web components following Trimble's Modus design system. This updated version focuses on improved flexibility, enhanced theming options, comprehensive cust
289 lines (284 loc) • 17.7 kB
JavaScript
'use strict';
var index = require('./index-CtEGR9Z7.js');
var baseComponent = require('./base-component-Dx-Crsr_.js');
var utils = require('./utils-B2oo-h7a.js');
const modusWcBottomSheetCss = "modus-wc-bottom-sheet.modus-wc-bottom-sheet{border-radius:var(--rounded-box) var(--rounded-box) 0 0;bottom:0;box-shadow:0 -20px 50px -8px color-mix(in sRGB, var(--modus-wc-color-base-content) 22%, transparent), 0 -6px 16px color-mix(in sRGB, var(--modus-wc-color-base-content) 12%, transparent);left:50%;max-width:100vw;min-width:25vw;position:fixed;transform:translateX(-50%);transition:transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0s linear 0.3s;z-index:999}modus-wc-bottom-sheet.modus-wc-bottom-sheet[aria-hidden=true]{box-shadow:none}modus-wc-bottom-sheet.modus-wc-bottom-sheet[aria-hidden=false]{transition:transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0s linear 0s}modus-wc-bottom-sheet.modus-wc-bottom-sheet:focus,modus-wc-bottom-sheet.modus-wc-bottom-sheet:focus-visible{outline:none}modus-wc-bottom-sheet.modus-wc-bottom-sheet.modus-wc-bottom-sheet-minimized .modus-wc-panel .modus-wc-bottom-sheet-header,modus-wc-bottom-sheet.modus-wc-bottom-sheet.modus-wc-bottom-sheet-minimized .modus-wc-panel .modus-wc-bottom-sheet-content,modus-wc-bottom-sheet.modus-wc-bottom-sheet.modus-wc-bottom-sheet-minimized .modus-wc-panel .modus-wc-bottom-sheet-footer{display:none}modus-wc-bottom-sheet.modus-wc-bottom-sheet .modus-wc-panel{border-radius:var(--rounded-box) var(--rounded-box) 0 0}modus-wc-bottom-sheet.modus-wc-bottom-sheet .modus-wc-panel .modus-wc-bottom-sheet-handle{cursor:grab;min-height:var(--modus-wc-font-size-2xl);touch-action:none}modus-wc-bottom-sheet.modus-wc-bottom-sheet .modus-wc-panel .modus-wc-bottom-sheet-handle:active{cursor:grabbing}modus-wc-bottom-sheet.modus-wc-bottom-sheet .modus-wc-panel .modus-wc-bottom-sheet-header{padding:var(--modus-wc-spacing-md);width:100%}modus-wc-bottom-sheet.modus-wc-bottom-sheet .modus-wc-panel .modus-wc-bottom-sheet-header-top{align-items:center;display:flex;gap:var(--modus-wc-spacing-lg);justify-content:space-between;width:100%}modus-wc-bottom-sheet.modus-wc-bottom-sheet .modus-wc-panel .modus-wc-bottom-sheet-header-start{align-items:center;display:flex;flex:1 1 0;gap:var(--modus-wc-spacing-xs);min-width:0}modus-wc-bottom-sheet.modus-wc-bottom-sheet .modus-wc-panel .modus-wc-bottom-sheet-header-title{line-height:var(--modus-wc-font-size-2xl)}modus-wc-bottom-sheet.modus-wc-bottom-sheet .modus-wc-panel .modus-wc-bottom-sheet-content{padding:0 var(--modus-wc-spacing-md) var(--modus-wc-spacing-md) var(--modus-wc-spacing-md)}modus-wc-bottom-sheet.modus-wc-bottom-sheet .modus-wc-panel .modus-wc-bottom-sheet-footer{padding:var(--modus-wc-spacing-md)}";
const ModusWcBottomSheet = class {
constructor(hostRef) {
index.registerInstance(this, hostRef);
this.sheetVisibilityChange = index.createEvent(this, "sheetVisibilityChange", 7);
this.displayModeChange = index.createEvent(this, "displayModeChange", 7);
this.headerBackClick = index.createEvent(this, "headerBackClick", 7);
this.headerCloseClick = index.createEvent(this, "headerCloseClick", 7);
this.inheritedAttributes = {};
this.startY = 0;
this.startHeight = 0;
this.currentDelta = 0;
/** Set when the sheet opens so focus can move inside it after the next render. */
this.pendingFocus = false;
/**
* The mode requested via the `displayMode` prop (as opposed to a drag/keyboard
* interaction). Reopening the sheet restores this value so the property always
* wins, discarding any live mode an earlier interaction left behind.
*/
this.propDisplayMode = 'default';
/**
* True only while an interaction (drag/keyboard) writes `displayMode`, so the
* watch can tell interaction-driven changes apart from property changes.
*/
this.isInteractionChange = false;
/** Custom CSS class to apply to the outer div. */
this.customClass = '';
/** Controls whether the bottom sheet is visible. */
this.visible = false;
/** Resting display mode: 'minimized', 'default', or 'expanded'. Drag/keyboard interactions do not overwrite this prop. */
this.displayMode = 'default';
/** Fraction (0-1) of the sheet height it must be dragged, in either direction, before it steps one level. */
this.dragStepThreshold = 0.4;
this.isDragging = false;
this.dragOffset = 0;
this.dragHeight = null;
this.hasHeader = false;
this.hasFooter = false;
this.onPointerDown = (e) => {
if (!this.visible)
return;
e.preventDefault();
this.isDragging = true;
this.startY = e.clientY;
this.currentDelta = 0;
// The panel is always rendered, so the reference is non-null.
this.startHeight =
this.el.querySelector('.modus-wc-panel').offsetHeight;
document.addEventListener('pointermove', this.onPointerMove);
document.addEventListener('pointerup', this.onPointerUp);
};
this.onPointerMove = (e) => {
e.preventDefault();
// Keep the grab cursor while dragging (re-asserted after the handle's own mousedown).
document.body.style.cursor = 'grabbing';
this.currentDelta = e.clientY - this.startY;
if (this.currentDelta >= 0) {
// Dragging downward: the whole sheet follows the pointer.
this.dragOffset = this.currentDelta;
this.dragHeight = null;
}
else {
// Dragging upward: grow the sheet height live to preview the expand,
// clamped to the viewport height.
this.dragOffset = 0;
const grown = this.startHeight - this.currentDelta;
this.dragHeight = `${Math.min(grown, window.innerHeight)}px`;
}
};
this.onPointerUp = () => {
var _a;
document.removeEventListener('pointermove', this.onPointerMove);
document.removeEventListener('pointerup', this.onPointerUp);
document.body.style.cursor = '';
const delta = this.currentDelta;
this.isDragging = false;
this.dragOffset = 0;
this.dragHeight = null;
// The same fraction-of-height threshold governs both directions. It is based
// on the resting height captured at pointerdown (not the live height, which
// grows during an upward drag) so up and down behave symmetrically.
const stepThresholdPx = ((_a = this.dragStepThreshold) !== null && _a !== void 0 ? _a : 0.4) * this.startHeight;
if (delta > stepThresholdPx) {
// Drag down steps down one level (expanded -> default -> minimized).
// It never closes the sheet; closing is property/action driven only.
this.stepDown();
}
else if (-delta > stepThresholdPx) {
// Drag up steps up one level (minimized -> default -> expanded).
this.stepUp();
}
};
this.onHandleKeyDown = (e) => {
if (e.key === 'ArrowUp') {
e.preventDefault();
this.stepUp();
}
else if (e.key === 'ArrowDown') {
e.preventDefault();
this.stepDown();
}
else if (e.key === 'Escape') {
e.preventDefault();
this.setVisible(false);
}
};
this.onHeaderBackClick = () => {
this.headerBackClick.emit();
};
this.onHeaderCloseClick = () => {
this.setVisible(false);
this.headerCloseClick.emit();
};
}
handleVisibleChange(isVisible) {
var _a;
// Keep a closed sheet out of the tab order / a11y tree.
this.setInert(!isVisible);
if (isVisible) {
// Reopening must follow the `displayMode` property, discarding any live
// mode an earlier drag/keyboard interaction left behind on the last open.
if (((_a = this.displayMode) !== null && _a !== void 0 ? _a : 'default') !== this.propDisplayMode) {
this.displayMode = this.propDisplayMode;
}
// WCAG 2.4.3 (Focus Order): opening a dialog must move focus inside it.
// Defer to componentDidRender so the sheet is rendered (and no longer
// inert/aria-hidden) before focus moves.
this.pendingFocus = true;
}
// Emit here so both internal (setVisible) and external (prop) changes notify consumers.
this.sheetVisibilityChange.emit({ visible: isVisible });
}
handleDisplayModeChange(newValue) {
// A property-driven change (not a drag/keyboard interaction) becomes the
// mode that is restored on the next reopen.
if (!this.isInteractionChange) {
this.propDisplayMode = newValue !== null && newValue !== void 0 ? newValue : 'default';
}
// Only notify while the sheet is visible; mode changes made on a hidden sheet
// are not user-facing, so they should not emit.
if (this.visible) {
this.displayModeChange.emit({ displayMode: newValue });
}
}
componentWillLoad() {
var _a;
baseComponent.handleShadowDOMStyles(this.el);
this.inheritedAttributes = utils.inheritAriaAttributes(this.el);
// A closed sheet must not be focusable or in the a11y tree (@Watch does not
// fire on initial load, so the initial state is set here).
this.setInert(!this.visible);
// Remember the mode requested via the prop so reopening always restores it,
// even if a drag/keyboard interaction changes the live mode in between.
this.propDisplayMode = (_a = this.displayMode) !== null && _a !== void 0 ? _a : 'default';
// Captured before first render: the host's direct children are still the
// consumer-provided slotted nodes (Stencil relocates them into the panel
// once rendered, so this must run here).
const children = Array.from(this.el.children);
this.hasHeader = children.some((c) => c.getAttribute('slot') === 'header');
this.hasFooter = children.some((c) => c.getAttribute('slot') === 'footer');
}
componentDidRender() {
if (this.pendingFocus) {
this.pendingFocus = false;
// The host carries role="dialog" and an accessible name, so assistive
// technology announces the sheet when focus lands on it.
this.el.focus();
}
}
/** Toggle the `inert` attribute on the host so closed sheets cannot be focused. */
setInert(inert) {
if (inert) {
this.el.setAttribute('inert', '');
}
else {
this.el.removeAttribute('inert');
}
}
disconnectedCallback() {
// The handle's pointerdown/keydown listeners are bound in JSX, so Stencil
// tears them down automatically. Only the document-level drag listeners
// (added in onPointerDown) need manual cleanup in case of a mid-drag teardown.
document.removeEventListener('pointermove', this.onPointerMove);
document.removeEventListener('pointerup', this.onPointerUp);
// Reset the global grab cursor if the sheet is torn down mid-drag
// (onPointerUp, which normally clears it, will never fire).
if (this.isDragging) {
document.body.style.cursor = '';
}
}
/** Step up one rung of the ladder: minimized -> default -> expanded. */
stepUp() {
var _a;
const ladder = ModusWcBottomSheet.DISPLAY_MODE_LADDER;
const index = ladder.indexOf((_a = this.displayMode) !== null && _a !== void 0 ? _a : 'default');
this.setDisplayMode(ladder[Math.min(index + 1, ladder.length - 1)]);
}
/** Step down one rung of the ladder: expanded -> default -> minimized (never closes). */
stepDown() {
var _a;
const ladder = ModusWcBottomSheet.DISPLAY_MODE_LADDER;
const index = ladder.indexOf((_a = this.displayMode) !== null && _a !== void 0 ? _a : 'default');
this.setDisplayMode(ladder[Math.max(index - 1, 0)]);
}
setVisible(value) {
if (this.visible === value)
return;
// Assigning `visible` runs the @Watch('visible') handler, which emits
// sheetVisibilityChange (so it fires for both internal and external changes).
this.visible = value;
}
setDisplayMode(value) {
if (this.displayMode === value)
return;
// Mark this as interaction-driven so the watch does not adopt it as the new
// property mode; the prop's mode is what gets restored when the sheet reopens.
this.isInteractionChange = true;
this.displayMode = value;
this.isInteractionChange = false;
// @Watch('displayMode') emits displayModeChange.
}
getClasses() {
const classList = ['modus-wc-bottom-sheet'];
if (this.displayMode === 'expanded')
classList.push('modus-wc-bottom-sheet-expanded');
if (this.displayMode === 'minimized')
classList.push('modus-wc-bottom-sheet-minimized');
if (this.isDragging)
classList.push('modus-wc-bottom-sheet-dragging');
if (this.customClass)
classList.push(this.customClass);
return classList.join(' ');
}
getTransform() {
if (!this.visible)
return 'translate(-50%, 100%)';
if (this.dragOffset > 0)
return `translate(-50%, ${this.dragOffset}px)`;
return 'translate(-50%, 0)';
}
getPanelHeight() {
if (this.isDragging && this.dragHeight)
return this.dragHeight;
return this.displayMode === 'expanded' ? '95dvh' : 'auto';
}
hasDefaultHeader() {
if (!this.header)
return false;
const { title, subtitle, showBackButton, showCloseButton } = this.header;
return !!(title || subtitle || showBackButton || showCloseButton);
}
shouldRenderHeader() {
return this.hasHeader || this.hasDefaultHeader();
}
renderDefaultHeader(header) {
return (index.h("div", { class: "modus-wc-bottom-sheet-header-top" }, index.h("div", { class: "modus-wc-bottom-sheet-header-start" }, header.showBackButton && (index.h("modus-wc-button", { "aria-label": "Back", color: "tertiary", onButtonClick: this.onHeaderBackClick, shape: "square", size: "sm", variant: "borderless" }, index.h("modus-wc-icon", { name: "chevron_left", decorative: true, size: "xs" }))), (header.title || header.subtitle) && (index.h("div", null, header.title && (index.h("modus-wc-typography", { customClass: "modus-wc-bottom-sheet-header-title", hierarchy: "h2", size: "md", weight: "semibold", label: header.title })), header.subtitle && (index.h("modus-wc-typography", { hierarchy: "p", size: "sm", label: header.subtitle }))))), header.showCloseButton && (index.h("modus-wc-button", { "aria-label": "Close", color: "tertiary", onButtonClick: this.onHeaderCloseClick, shape: "square", size: "sm", variant: "borderless" }, index.h("modus-wc-icon", { name: "close", decorative: true, size: "xs" })))));
}
render() {
return (index.h(index.Host, Object.assign({ key: 'c6d1960ddd3906dc2790dac5810f54a6d2689734' }, this.inheritedAttributes, { class: this.getClasses(), role: "dialog", tabIndex: -1, "aria-hidden": (!this.visible).toString(), "aria-modal": this.visible ? 'true' : undefined, style: {
transform: this.getTransform(),
transition: this.isDragging ? 'none' : undefined,
} }), index.h("modus-wc-panel", { key: '2acc0e88b4e1d4c1ff049c267609719a00e3d83b', width: "100%", height: this.getPanelHeight() }, index.h("modus-wc-handle", { key: '0b6a88ea883b9d3ad73b1cd5c6237248fc29789f', slot: "header", customClass: "modus-wc-bottom-sheet-handle", density: "comfortable", orientation: "vertical", size: "default", type: "bar", onPointerDown: this.onPointerDown, onKeyDown: this.onHandleKeyDown }), this.shouldRenderHeader() && (index.h("div", { key: '6a4da636e245f4576e6d6bb581eafda959d4d08e', class: "modus-wc-bottom-sheet-header", slot: "header" }, this.hasHeader ? (index.h("slot", { name: "header" })) : (this.renderDefaultHeader(this.header)))), index.h("div", { key: 'fa4ea16fd7f45a86aa26133c283b4ab5eeec114f', class: "modus-wc-bottom-sheet-content", slot: "body" }, index.h("slot", { key: '36ba21d1bc911e3cb68ae6959918496a3efe0a8d', name: "content" })), this.hasFooter && (index.h("div", { key: '14dafb983a96f6ef88af3e5e52e38ed3dc5dab08', class: "modus-wc-bottom-sheet-footer", slot: "footer" }, index.h("slot", { key: '0b2d03011d2e07257c985dcc4cc5366f90a8d683', name: "footer" }))))));
}
get el() { return index.getElement(this); }
static get watchers() { return {
"visible": ["handleVisibleChange"],
"displayMode": ["handleDisplayModeChange"]
}; }
};
/**
* Ordered rungs used by drag/keyboard interactions. Stepping moves one rung at
* a time so the sheet never jumps straight from minimized to expanded.
*/
ModusWcBottomSheet.DISPLAY_MODE_LADDER = [
'minimized',
'default',
'expanded',
];
ModusWcBottomSheet.style = modusWcBottomSheetCss;
exports.modus_wc_bottom_sheet = ModusWcBottomSheet;