diff --git a/assets/js/expandable-table.js b/assets/js/expandable-table.js
new file mode 100644
index 00000000000..590968042e8
--- /dev/null
+++ b/assets/js/expandable-table.js
@@ -0,0 +1,110 @@
+(function () {
+ 'use strict';
+
+ var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
+
+ // Makes a scroll container keyboard accessible only when it actually scrolls
+ function updateScrollable(el, label) {
+ var scrolls = el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight;
+ if (scrolls) {
+ el.setAttribute('tabindex', '0');
+ el.setAttribute('role', 'region');
+ el.setAttribute('aria-label', label);
+ } else {
+ el.removeAttribute('tabindex');
+ el.removeAttribute('role');
+ el.removeAttribute('aria-label');
+ }
+ }
+
+ function tableLabel(wrap) {
+ return (wrap.dataset.title || 'Table') + ' (scrollable)';
+ }
+
+ function init() {
+ document.querySelectorAll('.expandable-table-btn').forEach(function (btn) {
+ btn.addEventListener('click', function () {
+ var wrap = btn.closest('.expandable-table-wrap');
+ var modal = wrap.nextElementSibling;
+ openModal(wrap, modal);
+ });
+ });
+
+ document.querySelectorAll('.expandable-table-wrap').forEach(function (wrap) {
+ var inner = wrap.querySelector('.expandable-table-inner');
+ var check = function () { updateScrollable(inner, tableLabel(wrap)); };
+ check();
+ if (window.ResizeObserver) {
+ new ResizeObserver(check).observe(inner);
+ } else {
+ window.addEventListener('resize', check);
+ }
+ });
+ }
+
+ function openModal(wrap, modal) {
+ var title = wrap.dataset.title || '';
+ modal.querySelector('.expandable-table-title').textContent = title;
+ if (title) modal.setAttribute('aria-label', title);
+
+ var body = modal.querySelector('.expandable-table-body');
+ body.innerHTML = '';
+ var clone = wrap.querySelector('.expandable-table-inner').cloneNode(true);
+ // The modal body is the scroll container, so the cloned wrapper must not be a second tab stop
+ clone.removeAttribute('tabindex');
+ clone.removeAttribute('role');
+ clone.removeAttribute('aria-label');
+ body.appendChild(clone);
+
+ modal.removeAttribute('hidden');
+ document.body.classList.add('expandable-table-open');
+ updateScrollable(body, tableLabel(wrap));
+
+ var closeBtn = modal.querySelector('.expandable-table-close');
+ closeBtn.focus();
+
+ function close() {
+ modal.setAttribute('hidden', '');
+ document.body.classList.remove('expandable-table-open');
+ btn.focus();
+ modal.querySelector('.expandable-table-overlay').removeEventListener('click', close);
+ document.removeEventListener('keydown', onKey);
+ }
+
+ var btn = wrap.querySelector('.expandable-table-btn');
+ closeBtn.onclick = close;
+ modal.querySelector('.expandable-table-overlay').addEventListener('click', close);
+
+ function onKey(e) {
+ if (e.key === 'Escape') {
+ close();
+ return;
+ }
+ if (e.key !== 'Tab') return;
+
+ // Keep focus inside the dialog while it is open
+ var items = modal.querySelector('.expandable-table-dialog').querySelectorAll(FOCUSABLE);
+ if (!items.length) return;
+ var first = items[0];
+ var last = items[items.length - 1];
+ var active = document.activeElement;
+ if (!modal.contains(active)) {
+ e.preventDefault();
+ first.focus();
+ } else if (e.shiftKey && active === first) {
+ e.preventDefault();
+ last.focus();
+ } else if (!e.shiftKey && active === last) {
+ e.preventDefault();
+ first.focus();
+ }
+ }
+ document.addEventListener('keydown', onKey);
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss
index 928d591c0f0..bc975dc6417 100644
--- a/assets/scss/_styles_project.scss
+++ b/assets/scss/_styles_project.scss
@@ -694,4 +694,154 @@ img.ToC-icon{
overflow-y: auto;
padding-top: 1.25rem;
}
-}
\ No newline at end of file
+}
+
+// Expandable table modal
+
+.expandable-table-wrap {
+ margin-bottom: $spacer;
+
+ .expandable-table-toolbar {
+ display: flex;
+ justify-content: flex-start;
+ margin-bottom: 0.25rem;
+ }
+
+ .expandable-table-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ font-size: 0.75rem;
+ padding: 0.2rem 0.5rem;
+ border: 1px solid $gray-border;
+ color: $link-color;
+ background: transparent;
+ border-radius: 4px;
+ white-space: nowrap;
+
+ &:hover {
+ background: $gray-100;
+ border-color: $link-color;
+ }
+
+ &:focus {
+ outline: 2px solid $link-color;
+ outline-offset: 2px;
+ }
+ }
+
+ .expandable-table-inner {
+ overflow-x: auto;
+
+ // Docsy makes tables display:block with overflow:auto; keep the wrapper as the only scroll container
+ table {
+ display: table;
+ overflow: visible;
+ width: auto;
+ min-width: 100%;
+ }
+ }
+}
+
+.expandable-table-inner[tabindex],
+.expandable-table-body[tabindex] {
+ &:focus-visible {
+ outline: 2px solid $link-color;
+ outline-offset: 2px;
+ }
+}
+
+body.expandable-table-open {
+ overflow: hidden;
+}
+
+.expandable-table-modal {
+ position: fixed;
+ inset: 0;
+ z-index: 1060;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &[hidden] { display: none; }
+
+ .expandable-table-overlay {
+ position: absolute;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.5);
+ }
+
+ .expandable-table-dialog {
+ position: relative;
+ z-index: 1;
+ width: 90%;
+ max-width: 1100px;
+ max-height: 85vh;
+ background: white;
+ border-radius: 6px;
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ }
+
+ .expandable-table-header {
+ padding: 0.75rem 1rem;
+ border-bottom: 1px solid $gray-border;
+ background: $gray-100;
+ display: grid;
+ grid-template-columns: 1fr minmax(0, auto) 1fr;
+ align-items: center;
+ gap: 1rem;
+
+ .expandable-table-title {
+ grid-column: 2;
+ grid-row: 1;
+ text-align: center;
+ font-weight: 600;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .expandable-table-close {
+ grid-column: 1;
+ grid-row: 1;
+ justify-self: start;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ font-size: 0.875rem;
+
+ &:focus {
+ outline: 2px solid $link-color;
+ outline-offset: 2px;
+ }
+ }
+ }
+
+ .expandable-table-body {
+ overflow: auto;
+ padding: 1.25rem;
+
+ .expandable-table-inner {
+ border: none;
+ border-radius: 0;
+ overflow: visible;
+ display: flex;
+ }
+
+ // Auto margins center small tables in the flex container; shrinking lets wide tables wrap their text to fit the dialog
+ // The modal is outside .expandable-table-wrap, so reset Docsy's display:block/overflow:auto here too; the modal body must be the only (focusable) scroll container
+ .expandable-table-inner table { display: table; overflow: visible; flex: 0 1 auto; width: auto; max-width: 100%; margin: 0 auto !important; }
+ .expandable-table-inner th,
+ .expandable-table-inner td { white-space: normal; overflow-wrap: break-word; }
+ }
+}
+
+@include media-breakpoint-down(md) {
+ .expandable-table-modal .expandable-table-dialog {
+ width: 95%;
+ max-height: 90vh;
+ }
+}
diff --git a/content/en/docs/community-tools/contribute-to-mendix-docs/markdown-shortcodes.md b/content/en/docs/community-tools/contribute-to-mendix-docs/markdown-shortcodes.md
index 40861d9798f..46c3116ffb0 100644
--- a/content/en/docs/community-tools/contribute-to-mendix-docs/markdown-shortcodes.md
+++ b/content/en/docs/community-tools/contribute-to-mendix-docs/markdown-shortcodes.md
@@ -108,6 +108,49 @@ public class Hello1
2. Second list item
+### Expandable Tables
+
+Use the `expandable-table` shortcode to add an **Expand Table** button above a wide table. When readers click the button, the table opens in a window on the same page, which makes it easier to read. Place the table between the opening and closing tags, with a blank line after the opening tag and before the closing tag:
+
+{{% expandable-table %}}
+
+| Column1 | Column2 |
+| --- | --- |
+| Item1 | Item2 |
+| Item3 | Item4 |
+
+{{% /expandable-table %}}
+
+The optional `title` parameter sets the title shown at the top of the window. If you omit it or leave it blank, the window has no title.
+
+Here is an example:
+
+{{% expandable-table title="Title of the table" %}}
+
+| Column1 | Column2 |
+| --- | --- |
+| Item1 | Item2 |
+| Item3 | Item4 |
+
+{{% /expandable-table %}}
+
+#### Expandable Tables in Lists
+
+To use the shortcode in a list item, indent the shortcode tags and the table by four spaces, like any other content in the list item, and use the `<` delimiter instead of `%`. The shortcode removes the indentation from the table, so the table does not render as a code block:
+
+1. Step 1.
+
+ {{< expandable-table title="Title of the table" >}}
+
+ | Column1 | Column2 |
+ | --- | --- |
+ | Item1 | Item2 |
+ | Item3 | Item4 |
+
+ {{< /expandable-table >}}
+
+2. Continue with the next step.
+
### Images (Figures)
#### Image with Alt Text and Max-Width Class
diff --git a/content/en/docs/marketplace/platform-supported-content/modules/SAML/saml-installation-configuration.md b/content/en/docs/marketplace/platform-supported-content/modules/SAML/saml-installation-configuration.md
index c52b26ed9b8..84db7111986 100644
--- a/content/en/docs/marketplace/platform-supported-content/modules/SAML/saml-installation-configuration.md
+++ b/content/en/docs/marketplace/platform-supported-content/modules/SAML/saml-installation-configuration.md
@@ -62,6 +62,8 @@ If you use this method, do not forget to set the **SSOLandingPage** constant to
The table below introduces you to several key updates when you upgrade the SAML module from V3.x to V4.x.
+{{% expandable-table title="Changes in SAML Module Version 4.X" %}}
+
| Feature | Changes in Version 4.X |
| --- | --- |
| SSO Configuration | You can perform SSO configuration during design time and deploy time.
For versions below 4.2.0, the module introduced deploy-time configuration and `Custom_Create_IdPConfiguration` microflow for customized SSO configuration.
From version 4.2.0 onwards, you can instead use the `IdPConfiguration_MicroflowName` constant and configure your custom microflow name in it. |
@@ -70,6 +72,8 @@ The table below introduces you to several key updates when you upgrade the SAML
| InCommon Federation Support | Pre-configured support for InCommon Federation has been removed. You now need to create custom user provisioning microflows in version 4.0.0 |
| MxModeleReflection | In the **MxObjects_Overview** page, the MxModeleReflection module refresh is no longer required. It is handled automatically in the **SAML20.Startup**. |
+{{% /expandable-table %}}
+
## Choosing a SAML Configuration Approach{#config-approach}
The SAML module offers several ways to configure the Service Provider (SP) and Identity Provider (IdP). The best approach depends on when you want to define the configuration, how you deploy your applications, and whether you need to support one or multiple IdPs.
@@ -216,6 +220,8 @@ The following entities are used to create IdP configurations:
The table below shows you the different attributes and their values for quick reference. You can see the details of these attributes of the above entities in the [Reference Guide for SAML IdP Configuration](/appstore/modules/saml/idp-attributes/) document.
+{{% expandable-table title="IdP Configuration Attributes" %}}
+
| IDPConfiguration(Non-Persistable entity) | Description | Default Value |
| --- | --- | --- |
| **Alias** (mandatory) | This represents IdPconfiguration Alias | |
@@ -254,6 +260,8 @@ The table below shows you the different attributes and their values for quick re
| **EncryptionKeyLength** | This constant represents the Encryption length | 2048 bits |
| Active | After completion of IdP config it will make the Toggle Active | true |
+{{% /expandable-table %}}
+
Deploy the application and log in with the SSO. For more information, see the [Deploy the Application and Login with SSO](#deploy-application) section above.
### Manual Configuration{#runtime-config}
diff --git a/content/en/docs/marketplace/platform-supported-content/modules/oidc.md b/content/en/docs/marketplace/platform-supported-content/modules/oidc.md
index 00aae8bbe08..b7e32c58a29 100644
--- a/content/en/docs/marketplace/platform-supported-content/modules/oidc.md
+++ b/content/en/docs/marketplace/platform-supported-content/modules/oidc.md
@@ -201,6 +201,8 @@ If your app is already developed using Mendix 9 or above, but uses the community
This section provides an overview of updates for the OIDC SSO module across different versions. It includes new dependencies, snippet replacements, and microflow renaming to ensure a smooth transition while migrating to higher module versions.
+{{% expandable-table %}}
+
| Mendix Version | OIDC SSO Module Version | Important Migration Changes | Additional Information |
| --- | --- | --- | --- |
| 10.24.0 and above | 5.0.0 | - | Use `Token` as the parameter name in custom microflows or if you create a custom microflow based on `ACT_Token_CustomATPRetrieveRoles`. It was previously `AccessToken`. |
@@ -225,6 +227,8 @@ This section provides an overview of updates for the OIDC SSO module across diff
| 9.24.2 and above | 3.0.0 (migrating to 3.0.0 and above) | Include [UserCommons](https://marketplace.mendix.com/link/component/223053) module as a dependency. | New UserCommons module |
| | | Set `OIDC.Startup` microflow as part of the after-startup microflow. | Assign UserProvisioning for existing IdP configurations. |
+{{% /expandable-table %}}
+
## Design-time App Configuration{#app-configuration}
This section shows you how to configure your app to use OIDC for SSO.
@@ -237,11 +241,15 @@ If you are using OIDC module version 3.1.0 and above, you need to configure your
Ensure that you have allocated the following user roles to the OIDC module and UserCommons (in version 3.0.0 and above) roles:
+{{% expandable-table %}}
+
| User Role | OIDC Module Role |
| --- | --- |
| Administrator | OIDC.Administrator, UserCommons.Administrator |
| User | OIDC.User |
+{{% /expandable-table %}}
+
{{< figure src="/attachments/appstore/platform-supported-content/modules/oidc/user-roles.png" alt="User roles tab showing Administrator and User roles with their assigned module roles" >}}
### End User Login When Using Single or Multiple IdPs
@@ -367,12 +375,16 @@ In this case, the OIDC client is the app you are making.
9. On the **Creating User** tab, select your **User creation microflow**. By default, this module will use standard OpenID claims to provision end-users in your app. Also included is a flow that uses the standard UserInfo endpoint in OIDC, which is useful in the case that your IdP uses opaque tokens. You can set up user provisioning by setting the following standard flows:
+ {{< expandable-table >}}
+
| Default Microflow | Use |
| --- | --- |
| OIDC_CustomUserParsing_Standard
(renamed from UserProvisioning_Standard) | It implements some standard OpenID claims to find/provision a user. |
| OIDC_CustomUserParsing_UserInfo
(renamed from UserProvisioning_UserInfo) | It is similar as standard OIDC user parsing flow, except it works with identity providers that use `opaque` tokens. |
| OIDC_CustomUserParsing_Salesforce
(renamed from UserProvisioning_Salesforce) | It offers an `id` endpoint that retrieves information about user. You can use OpenID token (`id_token`) to map user attributes. |
+ {{< /expandable-table >}}
+
In version below 3.0.0 of the OIDC SSO module, you can configure the timezone and language using the `OIDC_CustomUserParsing_Standard` and `OIDC_CustomUserParsing_UserInfo` microflow. However, in version 3.0.0 and above of the OIDC SSO module, you can set the timezone and language using any standard microflow.
You can also use your own custom user entity to manage users of the app. See the section on [User Provisioning Using Your Custom User Entity](#custom_user_entity) for more information on what you can do to implement provisioning logic which fits your business needs. The module includes a Salesforce-specific example.
@@ -623,6 +635,8 @@ You can set up custom user provisioning by setting constants when you deploy you
You can set up custom user provisioning by setting the following constants. You can set default values when you build your app, but you can override these in the app's environment.
+{{% expandable-table %}}
+
| Constant | Use | Notes | Example |
| --- | --- | --- | --- |
| `CustomUserEntity` | a custom user entity | in the form `modulename.entityname` – a specialization of `System.User` | `Administration.Account` |
@@ -635,6 +649,8 @@ You can set up custom user provisioning by setting the following constants. You
| `CustomUserProvisioning` | a custom microflow to use for user provisioning | *optional* – in the form `modulename.microflowname` – the microflow name must begin with the string `UC_CustomProvisioning` | `Mymodule.UC_CustomProvisioning` |
| `DisableMxAdmin` | deactivates Mx admin | *optional* | `True` |
+{{% /expandable-table %}}
+
{{% alert color="info" %}}
You may have a requirement that users log in to your application only via SSO. However, when you deploy your app on the Mendix Cloud, the platform may still create an MxAdmin user with a local password. From version 2.1.0 of the UserCommons module, if the flag for the `DisableMxAdmin` constant is set to `True`, the MxAdmin user will be deactivated via the startup microflow `ASU_UserCommons_StartUp`.
{{% /alert %}}
@@ -690,12 +706,16 @@ If the standard configuration meets your needs and your application does not hav
In the default configuration, the custom user entity is set as `Administration.Account`, the principal attribute is set as `Name`, and the default attribute mapping is provided.
+{{% expandable-table title=" Default Attribute Mapping"%}}
+
| IdP Attribute | Configured Entity Attribute |
| -------------------- | --------------------------- |
| email | Email |
| name | FullName |
| sub | Name |
+{{% /expandable-table %}}
+
##### Modifying Default Attribute Mapping{#modify-default}
You may need a different or custom attribute mapping, for example, if you are configuring OIDC SSO and SCIM together and need a common identifier. For more information, see the [User Identifiers in the OIDC and SCIM Protocols](#user-identifiers-in-the-oidc-and-scim-protocols) section below.
@@ -759,12 +779,16 @@ The section below shows the methods to configure user provisioning when using OI
By default, the `CUSTOM_UserProvisioning` microflow in the **USE_ME** > **1. Configuration** folder of the OIDC module uses the `OIDC_CustomUserParsing_Standard` microflow. This applies to the following mapping:
+{{% expandable-table %}}
+
| ID-token Provided by your IdP | Attribute of `Administration.Account` Object |
| ----------------------------- | ----------------------------- |
| sub | Name |
| name | Fullname |
| email | Email |
+{{% /expandable-table %}}
+
{{% alert color="warning" %}}
Do not change the `UserProvisioning_StandardOIDC` microflow. This may cause problems if you upgrade to a newer version of the OIDC SSO module. Apply customizations to the `CUSTOM_UserProvisioning` microflow only.
{{% /alert %}}
diff --git a/layouts/partials/scripts.html b/layouts/partials/scripts.html
index ea4a2b7fbea..903513d9a8a 100644
--- a/layouts/partials/scripts.html
+++ b/layouts/partials/scripts.html
@@ -62,7 +62,7 @@
{{ $jsAnchor := resources.Get "js/anchor.js" -}}
-{{ $jsLodashDebouncd := resources.Get "js/lodash-debounce.js" }}
+{{ $jsLodashDebounced := resources.Get "js/lodash-debounce.js" }}
{{ $jsLodashThrottle := resources.Get "js/lodash-throttle.js" }}
{{ $jsProgressTracking := resources.Get "js/progress-tracking.js" }}
@@ -74,8 +74,9 @@
{{ $jsSearch = resources.Get "js/offline-search.js" -}}
{{ end -}}
-
-{{ $jsArray := slice $jsBs $jsBase $jsAnchor $jsLodashDebouncd $jsLodashThrottle $jsProgressTracking $jsSearch $jsPlantuml $jsMarkmap $jsDrawio -}}
+{{ $jsExpandableTable := resources.Get "js/expandable-table.js" }}
+
+{{ $jsArray := slice $jsBs $jsBase $jsAnchor $jsLodashDebounced $jsLodashThrottle $jsProgressTracking $jsExpandableTable $jsSearch $jsPlantuml $jsMarkmap $jsDrawio -}}
{{ if .Page.Store.Get "hasmermaid" -}}
{{- partial "scripts/mermaid.html" . -}}
diff --git a/layouts/shortcodes/expandable-table.html b/layouts/shortcodes/expandable-table.html
new file mode 100644
index 00000000000..6b7ac914434
--- /dev/null
+++ b/layouts/shortcodes/expandable-table.html
@@ -0,0 +1,29 @@
+{{ $_hugo_config := `{ "version": 1 }` }}
+