From d832b270af6980779ea2a4e9af8b8c28bb766e48 Mon Sep 17 00:00:00 2001 From: anatolii Date: Mon, 3 Aug 2026 12:42:19 +0300 Subject: [PATCH] feat(experience-lwc-generate): add native lightning/modal example, mark custom composable modal as fallback --- skills/experience-lwc-generate/SKILL.md | 3 +- .../nativeModalComponent.html | 21 +++++++ .../nativeModalComponent.js | 62 +++++++++++++++++++ .../nativeModalComponent.js-meta.xml | 13 ++++ .../references/component-patterns.md | 4 +- 5 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 skills/experience-lwc-generate/assets/native-modal-component/nativeModalComponent.html create mode 100644 skills/experience-lwc-generate/assets/native-modal-component/nativeModalComponent.js create mode 100644 skills/experience-lwc-generate/assets/native-modal-component/nativeModalComponent.js-meta.xml diff --git a/skills/experience-lwc-generate/SKILL.md b/skills/experience-lwc-generate/SKILL.md index 2d2ce41..67bed3e 100644 --- a/skills/experience-lwc-generate/SKILL.md +++ b/skills/experience-lwc-generate/SKILL.md @@ -168,7 +168,8 @@ Local Dev commands install just-in-time on first run. They are long-running proc - [assets/jest-test/componentName.test.js.example](assets/jest-test/componentName.test.js.example) — Jest test template (copy and rename, remove `.example` suffix) - [assets/message-channel/lmsPublisher.js](assets/message-channel/lmsPublisher.js) — LMS publisher pattern - [assets/message-channel/lmsSubscriber.js](assets/message-channel/lmsSubscriber.js) — LMS subscriber pattern -- [assets/modal-component/modalComponent.js](assets/modal-component/modalComponent.js) — modal with focus trap and ESC handling +- [assets/native-modal-component/nativeModalComponent.js](assets/native-modal-component/nativeModalComponent.js) — default modal pattern using the native `lightning/modal` service +- [assets/modal-component/modalComponent.js](assets/modal-component/modalComponent.js) — custom composable modal (focus trap, ESC handling); only for cases `lightning/modal` can't cover - [assets/record-picker/recordPicker.js](assets/record-picker/recordPicker.js) — record picker with search - [assets/state-store/store.js](assets/state-store/store.js) — reactive state store for cross-component state - [assets/typescript-component/typescriptComponent.ts](assets/typescript-component/typescriptComponent.ts) — TypeScript-enabled component (Spring '26) diff --git a/skills/experience-lwc-generate/assets/native-modal-component/nativeModalComponent.html b/skills/experience-lwc-generate/assets/native-modal-component/nativeModalComponent.html new file mode 100644 index 0000000..ef0c1e3 --- /dev/null +++ b/skills/experience-lwc-generate/assets/native-modal-component/nativeModalComponent.html @@ -0,0 +1,21 @@ + diff --git a/skills/experience-lwc-generate/assets/native-modal-component/nativeModalComponent.js b/skills/experience-lwc-generate/assets/native-modal-component/nativeModalComponent.js new file mode 100644 index 0000000..de418ca --- /dev/null +++ b/skills/experience-lwc-generate/assets/native-modal-component/nativeModalComponent.js @@ -0,0 +1,62 @@ +/** + * NATIVE MODAL TEMPLATE (lightning/modal) + * + * Uses the platform's built-in modal service instead of a hand-rolled + * backdrop/focus-trap implementation. LightningModal already provides: + * - focus trap and initial focus + * - ESC-to-close + * - ARIA roles/labels + * - size variants (small / medium / large / full) + * - a promise-based result returned from open() + * + * Supported in Lightning Experience, the Salesforce app, and Experience + * Builder sites (confirm current support for your specific site template + * before relying on it as the only implementation). + * + * Replace: nativeModalComponent → yourModalName + * Replace: NativeModalComponent → YourModalName + * + * ── How a parent opens this modal ────────────────────────────────────── + * import YourModalName from 'c/yourModalName'; + * + * async handleOpenModal() { + * const result = await YourModalName.open({ + * size: 'small', // small | medium | large | full + * description: 'Accessible description of the modal purpose', + * label: 'Confirm Action', // passed through as @api label + * content: 'Are you sure?' // any custom @api input props + * }); + * + * if (result === 'save') { + * // user confirmed + * } + * } + * ──────────────────────────────────────────────────────────────────────── + */ +import { api } from 'lwc'; +import LightningModal from 'lightning/modal'; + +export default class NativeModalComponent extends LightningModal { + // ═══════════════════════════════════════════════════════════════════════ + // PUBLIC API (@api) - Passed in via YourModalName.open({ ... }) + // ═══════════════════════════════════════════════════════════════════════ + + @api label; + @api content; + + // ═══════════════════════════════════════════════════════════════════════ + // EVENT HANDLERS + // ═══════════════════════════════════════════════════════════════════════ + + handleCancel() { + // Resolve the promise returned by open() with a falsy/known value + this.close('cancel'); + } + + handleSave() { + // Resolve the promise returned by open() with the result the + // caller needs. Close is explicit, so validation can prevent it: + // guard this call behind your own validity check before closing. + this.close('save'); + } +} diff --git a/skills/experience-lwc-generate/assets/native-modal-component/nativeModalComponent.js-meta.xml b/skills/experience-lwc-generate/assets/native-modal-component/nativeModalComponent.js-meta.xml new file mode 100644 index 0000000..9f7bfb4 --- /dev/null +++ b/skills/experience-lwc-generate/assets/native-modal-component/nativeModalComponent.js-meta.xml @@ -0,0 +1,13 @@ + + + + 66.0 + false + diff --git a/skills/experience-lwc-generate/references/component-patterns.md b/skills/experience-lwc-generate/references/component-patterns.md index b1d3a6b..0d17a5c 100644 --- a/skills/experience-lwc-generate/references/component-patterns.md +++ b/skills/experience-lwc-generate/references/component-patterns.md @@ -612,7 +612,9 @@ const BATCH_CREATE = gql` ## Modal Component Pattern -Based on [James Simone's composable modal pattern](https://www.jamessimone.net/blog/joys-of-apex/lwc-composable-modal/). +**Default to the native `lightning/modal` service** — see [assets/native-modal-component](../assets/native-modal-component/nativeModalComponent.js). It ships focus trap, ESC-to-close, ARIA, and size variants out of the box, so there's nothing to reinvent. Confirm it's supported on the target surface (some Experience Builder site templates historically lagged Lightning Experience support) before falling back to a custom implementation. + +Only reach for a custom composable modal below when `lightning/modal` genuinely can't cover the case (e.g. a confirmed gap on your site template, or composition needs beyond what `LightningModal` slots support). It's based on [James Simone's composable modal pattern](https://www.jamessimone.net/blog/joys-of-apex/lwc-composable-modal/); see [assets/modal-component](../assets/modal-component/modalComponent.js) for the fuller version with cleanup and multi-slot composition. Prefer a `save`/`cancel` `CustomEvent` over a function-reference `@api` prop for the save handler — function references can't be passed from Aura markup, which breaks on Aura-based Experience Builder pages. ```javascript // composableModal.js