# ModalBuilder JSON Format

`ModalBuilder` builds draggable modals from a JSON schema. The schema has optional `title` and three sections:

```json
{
  "title": "Demo Modal",
  "header": [ { "type": "html", "html": "<h3>Title</h3>" } ],
  "body": [
    { "type": "form", "formId": 5, "options": { } },
    { "type": "table", "options": { "tableName": "users", "editFormId": 5, "syncManager": {} } }
  ],
  "footer": [
    { "type": "button", "label": "Run", "workflow": "user.created" }
  ]
}
```

### Element Types
- **form** – uses `FormRenderer` to render a stored form when `formId` is
  provided. Inline `metadata` is still supported for ad‑hoc forms. `options`
  are passed through to `FormRenderer`.
 - **table** – uses `DataLoader` with provided `options`. Include `editFormId` inside the options block to load a custom form when editing a row. If `editFormId` is omitted, `ModalBuilder` searches the modal header for a button with `formName` or `formId`, resolves that form through `ModuleLoader.getDefaultManager()` and applies its id as the table's `editFormId`. Add `rowActions` in the options to append custom buttons per row. Use `filters` to supply predefined column filters (e.g., `{ "status": "open" }`).
- **html** – injects raw HTML.
- **button** – renders a button which can trigger a workflow (`workflow`), make an API call (`api: {url, method, body}`), load a form (`formName`/`formId`), open a wizard (`wizardId`/`wizardName`) or execute `onClick`.
  When using `onClick`, you can pass a function reference directly or a string name of a global function. If a string is provided, `ModalBuilder` looks up `window[onClick]` and attaches it as the click handler, calling the function with `(builder, event)` so helpers can access the current modal instance.
  Buttons with `wizardId` or `wizardName` call `openWizard()` when clicked. When only `wizardName` is supplied, `ModalBuilder` fetches `../api/form_wizards` to look up the matching wizard id.
- **tabs** – renders a set of tabs. Provide `tabs: [{label, elements}]` where `elements` is a list of child elements rendered when the tab is active.
- **calendar** – displays an interactive calendar. By default it loads events from the `calendar_events` table. Include `tableName` or `eventsApi` inside the `options` block to pull events from a different table or API endpoint.
- **graph-map** – renders a Google Maps heatmap. Set `options.provider` to `google` and optionally include `apiKey`, `libraries`, `data` or `endpoint`. When using the Google provider `ModuleLoader.loadMapLibraries()` injects the Maps script before rendering. If the endpoint returns a wrapper object, use `options.dataPath` to specify the property containing the array of `{lat,lng}` rows.
- **graph-chart** – displays a Chart.js chart. Provide `options.type` and `options.data` matching the Chart.js configuration or supply an `endpoint` that returns the data JSON. Set `options.dataPath` when the endpoint wraps the chart data under another property. Additional `chartOptions` are passed through to the chart constructor.
- **table-view** – *Deprecated*. Table views have been removed from the system.
  Use a standard `table` element with `DataLoader` instead.

Example:

```json
{ "type": "table", "options": { "tableName": "my_table", "filters": { "status": "open" } } }
```

Buttons with `formName` or `formId` automatically open the referenced form in a new modal. The form definition is retrieved via `LocalSyncManager` (which syncs the `forms` table from `/api/forms`) and rendered with a new `ModalBuilder` instance. The modal title uses `formTitle` if provided, otherwise the button's `label`.

Buttons with `wizardId` or `wizardName` behave similarly but launch a multi-step wizard. See `docs/MultiStepForms.md` for details on defining wizards. Example:

```json
{ "type": "button", "label": "Add Incident", "wizardId": 1 }
{ "type": "button", "label": "Onboard", "wizardName": "Employee Onboarding" }
```

Tables may include a `rowActions` array in the options block to add similar action buttons for each row:

```json
{ "type": "table", "options": {
  "tableName": "employees",
  "rowActions": [
    { "label": "Details", "formId": 10 },
    { "label": "Activate", "workflow": "employee.activate" }
  ]
} }
```

Forms should be created with the Form Builder and stored in the `forms` table. A modal only references the form by `formId` so the same definition can be reused across multiple modals.

Forms rendered through `ModalBuilder` also support **select-addable** and
**multiselect-search** fields. When the “+” button is used in a select-addable
dropdown, the component syncs the lookup table and refreshes the options so the
new value shows up immediately.

`ModalBuilder` automatically calls `makeModalInteractive` so the modal can be dragged and resized.
`makeModalInteractive` also assigns an incrementing `z-index` to each modal and disables pointer events on the overlay while enabling them on the modal box. This allows multiple modals to remain open and stacked.

### Map Libraries

Include a `mapLibraries` property to automatically load mapping scripts before the modal renders. Currently the only supported provider is Google Maps:

```json
{
  "title": "Location Editor",
  "mapLibraries": { "provider": "google", "libraries": "drawing" },
  "body": [ { "type": "table", "options": { "tableName": "time_clock_locations" } } ]
}
```

The `apiKey` and `libraries` fields are optional. When present, `ModuleLoader` injects the corresponding script tag once so multiple modules can share the same Google Maps instance.

## Loading Example via AJAX

`Dashboard.php` can fetch a modal description and render it dynamically.
The included `docs/example_ui_modal.json` demonstrates a header, table and form
inside the body plus footer buttons.  Load it like so:

```javascript
import { ModalBuilder } from './JsLibs2/core/ModalBuilder.js';
$(function () {
  $.getJSON('../docs/example_ui_modal.json', cfg => {
    const builder = new ModalBuilder(cfg);
    builder.render('#module-modal');
  });
});
```

This fetches the JSON and renders it into the `#module-modal` container on the
Dashboard page.

## ModuleLoader Integration

`ModuleLoader` reads modal configurations from the `ui_modals` table and renders
them through `ModalBuilder.render`. This ensures the modal header and footer are
always included, even when the target element is inside `#module-modal` (such as
`#work-area`). The resulting modal remains draggable and resizable because
`ModalBuilder.render` invokes `makeModalInteractive` internally.
Use `closeAnyModal()` (available globally as `closeModal`) to close the frontmost modal when multiple are stacked.

When you only need the configuration JSON, call `fetchConfig()` on a
`ModuleLoader` instance. The method initializes the same sync manager used by
`load()` and searches `ui_modals` for a matching record, syncing once if
necessary. It returns the modal definition (or `null` if not found) without
rendering anything.

Legacy modules are now filtered by the server-side `AuthorizationEngine`. It returns only the modals the current user may access, so authorized custom modules load dynamically rather than being hardcoded in the dashboard.

Saved detail views created with `DetailViewBuilder` can be injected into a modal
body. Fetch the view from `/api/detail_views/{id}`, render the modal and call
`DetailViewBuilder.mount()` followed by `loadConfig(view.config)` to populate the
content. See `docs/DetailViewBuilder.md` for a code example.

When the `detail-view` element also specifies `tableName` and `filterField`, `ModalBuilder` fetches the referenced record using the current filter value before rendering the view. It listens for `builderFilterChanged` events and refetches the record whenever the filter updates so **Aggregate** components stay in sync.

Detail-view items support a `label` field to prefix the displayed value. When a label is provided, ModalBuilder renders two child `<span>` elements inside the container. Use `label_classes` to apply classes only to the label span and optionally `value_classes` to style the value span.

```json
{ "type": "paragraph", "data_column": "client.phone", "label": "Phone", "label_classes": "font-bold" }
```

Produces:

```html
<p><span class="font-bold">Phone:</span><span>555-1234</span></p>
```

### Dashboard Card Permissions Example

The `dashboard_card_permissions` modal demonstrates the built-in `tabs` element. Each tab lists groups or users with checkboxes linked to the `dashboard_card_permissions` API.

```json
{
  "title": "Dashboard Card Permissions",
  "body": [
    { "type": "html", "html": "<select id=\"dcpTenantSelect\"><option value=\"\">All</option></select>" },
    {
      "type": "tabs",
      "tabs": [
        {
          "label": "Groups",
          "elements": [
            { "type": "table", "id": "dcpGroups", "options": { "tableName": "groups", "addButton": false } }
          ]
        },
        {
          "label": "Users",
          "elements": [
            { "type": "table", "id": "dcpUsers", "options": { "tableName": "users", "addButton": false } }
          ]
        }
      ]
    }
  ],
  "footer": [ { "type": "button", "label": "Close", "onClick": "closeModal" } ]
}
```

Selecting a tenant from the dropdown posts to the `tenants/switchContext` API
and reloads both tables so any changes appear right away.

Before committing changes to modal definitions run `./test_setup.sh`, `phpunit -c api/phpunit.xml` and `npm test`.

### Card Permissions Manager

Use `ModuleLoader('dashboard_card_permissions_manager')` to load the manager
modal. The helper `cardPermissions.js` exposes `initManager` and
`openCardPermsDetail` to populate dropdowns and open the detail grids.

`cardPermissions.js` clones the Dashboard cards into the element with id
`dcpmCards`. Each card displays a checkbox that reflects the selected tenant,
group or user permission. Toggling a checkbox immediately updates the
`dashboard_card_permissions` record.

If no permission record exists for a group or user, the checkbox appears
unchecked so administrators must explicitly enable access.

```json
{
  "title": "Card Permissions Manager",
  "body": [
    {
      "type": "html",
      "html": "<select id=\"dcpTenantSelect\"></select><select id=\"dcpmGroupSelect\" class=\"ml-2\"></select><select id=\"dcpmUserSelect\" class=\"ml-2\"></select>"
    },
    { "type": "html", "id": "dcpmCards", "html": "" },
    { "type": "html", "html": "<div id=\"dcpm-detail\" class=\"hidden\"></div>" }
  ],
  "footer": [
    { "type": "button", "label": "Save", "onClick": "saveCardPermissions" },
    { "type": "button", "label": "Close", "onClick": "closeModal" }
  ]
}
```

### filter-date-range

Adds two `<input type="date">` fields to filter tables by a date span.

```json
{ "type": "filter-date-range", "options": { "data_column": "logged_date" } }
```

The builder stores the selected range in `currentFilters[data_column]` as
`{ from, to }` and emits `builderFilterChanged` with
`{ data_column, values: { from, to } }` whenever either date changes.

---

## WCAG 2.1 AA Compliance

### Overview

ModalBuilder and DataLoader implement WCAG 2.1 Level AA accessibility standards to ensure keyboard navigation, screen reader support, and proper focus management.

### Compliance Checklist

#### ✅ Level A Requirements (Critical)

| Requirement | Status | Implementation |
|------------|--------|----------------|
| **2.1.1 Keyboard** | ✅ Complete | All functionality available via keyboard |
| **2.1.2 No Keyboard Trap** | ✅ Complete | Focus trap with Escape key exit |
| **2.4.3 Focus Order** | ✅ Complete | Logical tab order throughout modals |
| **2.4.7 Focus Visible** | ✅ Complete | Tailwind `focus-visible:ring` on all interactive elements |
| **4.1.2 Name, Role, Value** | ✅ Complete | ARIA labels, roles, and states properly set |

#### ✅ Level AA Requirements (Important)

| Requirement | Status | Implementation |
|------------|--------|----------------|
| **1.4.3 Contrast** | ✅ Complete | DaisyUI themes meet 4.5:1 ratio |
| **2.4.6 Headings & Labels** | ✅ Complete | Descriptive modal titles via `aria-labelledby` |
| **3.3.1 Error Identification** | ✅ Complete | Validation errors with `aria-describedby` |

### Focus Management

**Focus Trap Implementation:**
- When modal opens, focus moves to first interactive element (close button by default)
- Tab and Shift+Tab cycle through focusable elements within modal
- Focus cannot escape modal while open (focus trap active)
- Escape key closes modal and restores focus to previously focused element
- When modal closes, focus returns to trigger element

**Example:**
```javascript
// ModalBuilder automatically implements focus trap
const builder = new ModalBuilder({
    title: "Accessible Modal",
    body: [
        { type: "form", formId: 5 }
    ]
});
builder.render('#module-modal');
// Focus trap is active, Escape key exits
```

### ARIA Attributes

**Modal Dialog:**
```html
<div class="modal-box" role="dialog" aria-modal="true" aria-labelledby="modal-title-xyz">
    <h3 id="modal-title-xyz">Modal Title</h3>
    <button class="modal-close-btn" aria-label="Close dialog" title="Close (Esc)">✕</button>
    ...
</div>
```

**Table Elements:**
```html
<table role="table" aria-labelledby="table-caption">
    <caption id="table-caption" class="sr-only">Data table with sorting</caption>
    <thead>
        <tr><th scope="col" aria-sort="ascending">Name</th></tr>
    </thead>
    <tbody>
        <tr><td>Data</td></tr>
    </tbody>
</table>
```

**Loading States:**
```html
<div role="status" aria-live="polite" aria-label="Loading content">
    <span class="sr-only">Loading...</span>
</div>
```

### Keyboard Navigation

**Modal Shortcuts:**
- `Escape` - Close current modal
- `Tab` - Move to next focusable element (wraps to first)
- `Shift+Tab` - Move to previous focusable element (wraps to last)
- `Enter` - Activate focused button or link
- `Space` - Activate focused button

**Table Navigation:**
- `Arrow Keys` - Navigate between cells/rows
- `Home` - Jump to first row
- `End` - Jump to last row
- `Page Up/Down` - Scroll by page

### Visual Focus Indicators

All interactive elements use Tailwind CSS focus utilities:

```css
/* Applied automatically by ModalBuilder */
.focus-visible:outline-none
.focus-visible:ring-2
.focus-visible:ring-primary
.focus-visible:ring-offset-2
```

**High Contrast Mode Support:**
```css
@media (prefers-contrast: high) {
    .focus-visible:ring {
        outline: 3px solid;
        outline-offset: 2px;
    }
}
```

### Screen Reader Support

**Live Regions for Dynamic Content:**
```javascript
// Automatically announced to screen readers
showToast('Record saved successfully');
// Uses aria-live="polite" region
```

**Table Announcements:**
```html
<div role="status" aria-live="polite" class="sr-only">
    Showing 10 of 45 records
</div>
```

### Testing Guidelines

**Manual Testing:**
1. Navigate entire modal using only keyboard (Tab, Shift+Tab)
2. Verify focus trap prevents tabbing outside modal
3. Confirm Escape key closes modal and restores focus
4. Test with NVDA/JAWS/VoiceOver screen readers
5. Verify all interactive elements have visible focus indicators

**Automated Testing:**
1. Run `axe DevTools` on modal instances
2. Execute Lighthouse accessibility audit (target score > 95)
3. Use `Pa11y` in CI pipeline for regression testing

**Browser/AT Compatibility:**
- Chrome + NVDA: ✅ Full support
- Firefox + NVDA: ✅ Full support
- Edge + JAWS: ✅ Full support
- Safari + VoiceOver (Mac): ✅ Full support
- Safari + VoiceOver (iOS): ✅ Full support

### Resources

- [WCAG 2.1 Quick Reference](https://www.w3.org/WAI/WCAG21/quickref/)
- [ARIA Authoring Practices - Dialog Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)
- [axe DevTools](https://www.deque.com/axe/devtools/)
- [TAF Accessibility Testing Guide](../testing/accessibility.md)

**Last Updated:** October 4, 2025

