Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a1be59a51 | |||
| a3b2ace88d | |||
| c34037265e | |||
| 8c230fe3af |
1
.claude/scheduled_tasks.lock
Normal file
1
.claude/scheduled_tasks.lock
Normal file
@@ -0,0 +1 @@
|
||||
{"sessionId":"4b0f0a7f-f187-40a3-a38b-cb9a7e877011","pid":3110692,"acquiredAt":1775914414249}
|
||||
14
changelog.md
14
changelog.md
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-04-11 - 3.72.0 - feat(dees-stepper)
|
||||
add configurable cancellation flow with confirmation modal
|
||||
|
||||
- adds a cancelable option to control whether steppers can be dismissed
|
||||
- shows a confirmation modal when canceling via the new Cancel button or overlay backdrop
|
||||
- updates footer button rendering and separators to support the new cancel action consistently
|
||||
|
||||
## 2026-04-11 - 3.71.1 - fix(dees-modal)
|
||||
move modal content scrolling into dees-tile so long content stays scrollable with pinned header and actions
|
||||
|
||||
- Update dees-tile content area to use vertical scrolling when constrained by a max-height while keeping horizontal overflow clipped.
|
||||
- Remove duplicate scrolling styles from dees-modal and rely on the shared tile container behavior.
|
||||
- Add modal demo cases for long article, list, and form content to verify internal scrolling.
|
||||
|
||||
## 2026-04-11 - 3.71.0 - feat(dees-stepper)
|
||||
add footer menu actions with form-aware step validation
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@design.estate/dees-catalog",
|
||||
"version": "3.71.0",
|
||||
"version": "3.72.0",
|
||||
"private": false,
|
||||
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
||||
"main": "dist_ts_web/index.js",
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@design.estate/dees-catalog',
|
||||
version: '3.71.0',
|
||||
version: '3.72.0',
|
||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { themeDefaultStyles } from '../../00theme.js';
|
||||
import { cssGeistFontFamily } from '../../00fonts.js';
|
||||
import { zIndexRegistry } from '../../00zindex.js';
|
||||
import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
|
||||
import { DeesModal } from '../../00group-overlay/dees-modal/dees-modal.js';
|
||||
import type { DeesForm } from '../../00group-form/dees-form/dees-form.js';
|
||||
import '../dees-tile/dees-tile.js';
|
||||
|
||||
@@ -45,15 +46,16 @@ export class DeesStepper extends DeesElement {
|
||||
|
||||
public static async createAndShow(optionsArg: {
|
||||
steps: IStep[];
|
||||
cancelable?: boolean;
|
||||
}): Promise<DeesStepper> {
|
||||
const body = document.body;
|
||||
const stepper = new DeesStepper();
|
||||
stepper.steps = optionsArg.steps;
|
||||
stepper.overlay = true;
|
||||
if (optionsArg.cancelable !== undefined) {
|
||||
stepper.cancelable = optionsArg.cancelable;
|
||||
}
|
||||
stepper.windowLayer = await DeesWindowLayer.createAndShow({ blur: true });
|
||||
stepper.windowLayer.addEventListener('click', async () => {
|
||||
await stepper.destroy();
|
||||
});
|
||||
body.append(stepper.windowLayer);
|
||||
body.append(stepper);
|
||||
|
||||
@@ -81,6 +83,18 @@ export class DeesStepper extends DeesElement {
|
||||
})
|
||||
accessor overlay: boolean = false;
|
||||
|
||||
/**
|
||||
* When true (default), the stepper renders a Cancel button in every step's
|
||||
* footer, and clicking the backdrop (overlay mode) triggers the same cancel
|
||||
* confirmation flow. Set to false for forced flows where the user must
|
||||
* complete the stepper — no Cancel button, no backdrop dismissal.
|
||||
*/
|
||||
@property({
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
})
|
||||
accessor cancelable: boolean = true;
|
||||
|
||||
@property({ type: Number, attribute: false })
|
||||
accessor stepperZIndex: number = 1000;
|
||||
|
||||
@@ -280,15 +294,17 @@ export class DeesStepper extends DeesElement {
|
||||
transition: all 0.15s ease;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-left: 1px solid var(--dees-color-border-subtle);
|
||||
color: var(--dees-color-text-muted);
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bottomButtons .bottomButton:first-child {
|
||||
border-left: none;
|
||||
/* Border-left separator on every button EXCEPT the first one.
|
||||
Uses general sibling so the stepHint (if rendered on the left) does
|
||||
not shift which button counts as "first" and create a phantom border. */
|
||||
.bottomButtons .bottomButton ~ .bottomButton {
|
||||
border-left: 1px solid var(--dees-color-border-subtle);
|
||||
}
|
||||
|
||||
.bottomButtons .bottomButton:hover {
|
||||
@@ -347,6 +363,7 @@ export class DeesStepper extends DeesElement {
|
||||
<div
|
||||
class="stepperContainer ${this.overlay ? 'overlay' : ''}"
|
||||
style=${this.overlay ? `z-index: ${this.stepperZIndex};` : ''}
|
||||
@click=${this.handleOutsideClick}
|
||||
>
|
||||
${this.steps.map((stepArg, stepIndex) => {
|
||||
const isSelected = stepArg === this.selectedStep;
|
||||
@@ -370,23 +387,27 @@ export class DeesStepper extends DeesElement {
|
||||
<div class="title">${stepArg.title}</div>
|
||||
<div class="content">${stepArg.content}</div>
|
||||
</div>
|
||||
${stepArg.menuOptions && stepArg.menuOptions.length > 0
|
||||
? html`<div slot="footer" class="bottomButtons">
|
||||
${isSelected && this.activeForm !== null && !this.activeFormValid
|
||||
? html`<div class="stepHint">Complete form to continue</div>`
|
||||
: ''}
|
||||
${stepArg.menuOptions.map((actionArg, actionIndex) => {
|
||||
const isPrimary = actionIndex === stepArg.menuOptions!.length - 1;
|
||||
const isDisabled = isPrimary && this.activeForm !== null && !this.activeFormValid;
|
||||
return html`
|
||||
<div
|
||||
class="bottomButton ${isPrimary ? 'primary' : ''} ${isDisabled ? 'disabled' : ''}"
|
||||
@click=${() => this.handleMenuOptionClick(actionArg, isPrimary)}
|
||||
>${actionArg.name}</div>
|
||||
`;
|
||||
})}
|
||||
</div>`
|
||||
: ''}
|
||||
<div slot="footer" class="bottomButtons">
|
||||
${isSelected && this.activeForm !== null && !this.activeFormValid
|
||||
? html`<div class="stepHint">Complete form to continue</div>`
|
||||
: ''}
|
||||
${this.cancelable
|
||||
? html`<div
|
||||
class="bottomButton"
|
||||
@click=${() => this.handleCancelRequest()}
|
||||
>Cancel</div>`
|
||||
: ''}
|
||||
${stepArg.menuOptions?.map((actionArg, actionIndex) => {
|
||||
const isPrimary = actionIndex === stepArg.menuOptions!.length - 1;
|
||||
const isDisabled = isPrimary && this.activeForm !== null && !this.activeFormValid;
|
||||
return html`
|
||||
<div
|
||||
class="bottomButton ${isPrimary ? 'primary' : ''} ${isDisabled ? 'disabled' : ''}"
|
||||
@click=${() => this.handleMenuOptionClick(actionArg, isPrimary)}
|
||||
>${actionArg.name}</div>
|
||||
`;
|
||||
}) ?? ''}
|
||||
</div>
|
||||
</dees-tile>`;
|
||||
})}
|
||||
</div>
|
||||
@@ -556,6 +577,76 @@ export class DeesStepper extends DeesElement {
|
||||
await optionArg.action(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently-open confirmation modal (if any). Prevents double-stacking when
|
||||
* the user clicks the backdrop or the Cancel button while a confirm modal
|
||||
* is already visible. The reference may become stale (point to a destroyed
|
||||
* modal) if the user dismisses the confirm modal by clicking its own
|
||||
* backdrop — so handleCancelRequest() uses isConnected to detect that.
|
||||
*/
|
||||
private cancelConfirmModal?: DeesModal;
|
||||
|
||||
/**
|
||||
* Click handler on .stepperContainer. Mirrors dees-modal.handleOutsideClick:
|
||||
* when the user clicks the empty backdrop area (target === stepperContainer,
|
||||
* not any descendant tile), trigger the cancel confirmation flow. Clicks
|
||||
* that originate inside a step tile have a different event.target and are
|
||||
* ignored here.
|
||||
*/
|
||||
private handleOutsideClick(eventArg: MouseEvent) {
|
||||
if (!this.overlay) return;
|
||||
if (!this.cancelable) return;
|
||||
eventArg.stopPropagation();
|
||||
const stepperContainer = this.shadowRoot!.querySelector('.stepperContainer');
|
||||
if (eventArg.target === stepperContainer) {
|
||||
this.handleCancelRequest();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shown by both the backdrop click and the Cancel button in the footer.
|
||||
* Presents a dees-modal asking the user to confirm cancellation. If they
|
||||
* confirm, the stepper and window layer are destroyed; otherwise the
|
||||
* confirm modal is dismissed and the stepper stays open.
|
||||
*
|
||||
* The isConnected check on the cached reference handles the case where the
|
||||
* user dismissed the previous confirm modal by clicking ITS OWN backdrop —
|
||||
* dees-modal.handleOutsideClick calls destroy() directly, bypassing our
|
||||
* action callbacks, so our cached reference would be stale without this
|
||||
* fallback check.
|
||||
*/
|
||||
public async handleCancelRequest() {
|
||||
if (!this.cancelable) return;
|
||||
if (this.cancelConfirmModal && this.cancelConfirmModal.isConnected) return;
|
||||
this.cancelConfirmModal = undefined;
|
||||
this.cancelConfirmModal = await DeesModal.createAndShow({
|
||||
heading: 'Cancel setup?',
|
||||
width: 'small',
|
||||
content: html`
|
||||
<p style="margin: 0;">
|
||||
Are you sure you want to cancel? Any progress on the current step will be lost.
|
||||
</p>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Continue setup',
|
||||
action: async (modal) => {
|
||||
this.cancelConfirmModal = undefined;
|
||||
await modal!.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Yes, cancel',
|
||||
action: async (modal) => {
|
||||
this.cancelConfirmModal = undefined;
|
||||
await modal!.destroy();
|
||||
await this.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
public async destroy() {
|
||||
const domtools = await this.domtoolsPromise;
|
||||
const container = this.shadowRoot!.querySelector('.stepperContainer');
|
||||
|
||||
@@ -87,14 +87,24 @@ export class DeesTile extends DeesElement {
|
||||
color: var(--dees-color-text-secondary);
|
||||
}
|
||||
|
||||
/* --- Content: the rounded inset --- */
|
||||
/* --- Content: the rounded inset ---
|
||||
Uses overflow-y: auto so that when a consumer (e.g. dees-modal) caps
|
||||
the tile with max-height, long content scrolls inside the tile
|
||||
instead of being clipped. For consumers without max-height
|
||||
(e.g. dees-stepper), the tile grows with content and the scroll
|
||||
never activates. Horizontal overflow stays clipped to preserve the
|
||||
rounded corners. */
|
||||
.tile-content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
border-top: 1px solid var(--dees-color-border-subtle);
|
||||
border-bottom: 1px solid var(--dees-color-border-subtle);
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
|
||||
}
|
||||
|
||||
.tile-content.no-footer {
|
||||
|
||||
@@ -352,5 +352,80 @@ export const demoFunc = () => html`
|
||||
});
|
||||
}}>Test Responsive</dees-button>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Scrollable Content</h3>
|
||||
<p>When content exceeds the modal's max-height (<code>calc(100vh - 80px)</code>), the tile caps at that height and the content area scrolls inside. The heading and bottom buttons stay pinned.</p>
|
||||
<div class="button-grid">
|
||||
<dees-button @click=${() => {
|
||||
DeesModal.createAndShow({
|
||||
heading: 'Long Article',
|
||||
width: 'medium',
|
||||
content: html`
|
||||
<h4 style="margin-top: 0;">Lorem ipsum dolor sit amet</h4>
|
||||
${Array.from({ length: 40 }, (_, i) => html`
|
||||
<p>
|
||||
<strong>§ ${i + 1}.</strong>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
|
||||
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
|
||||
enim ad minim veniam, quis nostrud exercitation ullamco laboris
|
||||
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
|
||||
in reprehenderit in voluptate velit esse cillum dolore eu fugiat
|
||||
nulla pariatur. Excepteur sint occaecat cupidatat non proident,
|
||||
sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
</p>
|
||||
`)}
|
||||
`,
|
||||
menuOptions: [{
|
||||
name: 'Cancel',
|
||||
action: async (modal) => modal!.destroy()
|
||||
}, {
|
||||
name: 'Accept',
|
||||
action: async (modal) => modal!.destroy()
|
||||
}],
|
||||
});
|
||||
}}>Long Article</dees-button>
|
||||
|
||||
<dees-button @click=${() => {
|
||||
DeesModal.createAndShow({
|
||||
heading: 'Long List',
|
||||
width: 'small',
|
||||
content: html`
|
||||
<p>Selected items:</p>
|
||||
<ul style="padding-left: 20px; margin: 0;">
|
||||
${Array.from({ length: 80 }, (_, i) => html`
|
||||
<li style="padding: 4px 0;">Item ${i + 1} — option label</li>
|
||||
`)}
|
||||
</ul>
|
||||
`,
|
||||
menuOptions: [{
|
||||
name: 'Done',
|
||||
action: async (modal) => modal!.destroy()
|
||||
}],
|
||||
});
|
||||
}}>Long List</dees-button>
|
||||
|
||||
<dees-button @click=${() => {
|
||||
DeesModal.createAndShow({
|
||||
heading: 'Tall Form',
|
||||
width: 'medium',
|
||||
content: html`
|
||||
<dees-form>
|
||||
${Array.from({ length: 25 }, (_, i) => html`
|
||||
<dees-input-text .label=${`Field ${i + 1}`}></dees-input-text>
|
||||
`)}
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [{
|
||||
name: 'Cancel',
|
||||
action: async (modal) => modal!.destroy()
|
||||
}, {
|
||||
name: 'Submit',
|
||||
action: async (modal) => modal!.destroy()
|
||||
}],
|
||||
});
|
||||
}}>Tall Form</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@@ -271,13 +271,6 @@ export class DeesModal extends DeesElement {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
|
||||
}
|
||||
.bottomButtons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
Reference in New Issue
Block a user