From 68c7401f3f60836ea9012a12e6350d1044d3a4b2 Mon Sep 17 00:00:00 2001 From: "Adolfo R. Brandes" Date: Thu, 19 Mar 2026 08:31:24 -0300 Subject: [PATCH 1/5] docs: ADR for slot layouts over wrapping operations Document the decision to not implement a widget wrapping operation in frontend-base, recommending layout replacements as the architecturally preferred alternative. Co-Authored-By: Claude Opus 4.6 --- .../0011-no-slot-wrapping-operation.rst | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 docs/decisions/0011-no-slot-wrapping-operation.rst diff --git a/docs/decisions/0011-no-slot-wrapping-operation.rst b/docs/decisions/0011-no-slot-wrapping-operation.rst new file mode 100644 index 00000000..a0f1ae15 --- /dev/null +++ b/docs/decisions/0011-no-slot-wrapping-operation.rst @@ -0,0 +1,180 @@ +############################################ +Slot layouts instead of a wrapping operation +############################################ + +Status +====== + +Accepted + + +Context +======= + +Legacy ``@openedx/frontend-plugin-framework`` provided a ``Wrap`` plugin +operation that allowed a plugin to wrap an existing widget's rendered output +with an arbitrary React component. The wrapper received the original content +as a ``component`` prop and could add markup around it, conditionally render +it, or replace it entirely. + +The canonical example of this was +`frontend-plugin-aspects `_, +the Open edX analytics plugin. It used ``Wrap`` on the authoring MFE's +sidebar slots to implement a toggle between the default sidebar and an +analytics sidebar: + +.. code-block:: typescript + + // frontend-plugin-aspects/src/components/SidebarToggleWrapper.tsx + export const SidebarToggleWrapper = ({ component }: { component: ReactNode }) => { + const { sidebarOpen } = useAspectsSidebarContext(); + return !sidebarOpen && component; + }; + + // frontend-plugin-aspects/src/plugin-slots.ts + { + op: PLUGIN_OPERATIONS.Wrap, + widgetId: 'default_contents', + wrapper: SidebarToggleWrapper, + } + +When the user opened the Aspects sidebar, the wrapper hid the default sidebar +content; when the Aspects sidebar was closed, the default content reappeared. +The plugin also used an ``Insert`` operation to add its own sidebar widget to +the slot. + +With the introduction of ``frontend-base`` as the successor to both +``frontend-plugin-framework`` and ``frontend-platform``, the slot and widget +architecture was redesigned. The question arose as to whether a ``Wrap`` +operation should be carried forward into the new architecture. + + +Decision +======== + +We will **not** implement a slot wrapping operation in ``frontend-base``. The +use cases previously served by wrapping are better addressed by improving the +existing layout system. + +``useWidgets()`` will be enriched with helper methods that let layouts filter +widgets by identity (ID or role). Internally, this metadata already exists +throughout the widget pipeline; it is only stripped at the very end before being +returned to layouts. The enriched return value remains a ``ReactNode[]`` that +renders identically when used as-is (preserving backwards compatibility), but +adds methods like ``byId()``, ``withoutId()``, ``byRole()``, and +``withoutRole()`` for selective rendering. + +For the sidebar toggle use case, a plugin can replace the slot's layout and +add its own widget, using only existing operations: + +.. code-block:: typescript + + // Replace the sidebar layout with a toggle-aware layout + { + slotId: 'org.openedx.frontend.slot.authoring.outlineSidebar.v1', + op: LayoutOperationTypes.REPLACE, + component: AspectsSidebarLayout, + }, + // Add the Aspects sidebar as a widget + { + slotId: 'org.openedx.frontend.slot.authoring.outlineSidebar.v1', + id: 'org.openedx.frontend.widget.aspects.outlineSidebar', + op: WidgetOperationTypes.APPEND, + component: CourseOutlineSidebar, + }, + +Where the custom layout controls the toggle: + +.. code-block:: typescript + + const aspectsWidget = 'org.openedx.frontend.widget.aspects.outlineSidebar'; + + function AspectsSidebarLayout() { + const widgets = useWidgets(); + const { sidebarOpen } = useAspectsSidebarContext(); + + if (sidebarOpen) { + return <>{widgets.byId(aspectsWidget)}; + } + return <>{widgets.withoutId(aspectsWidget)}; + } + +This approach is preferable for several reasons: + +Separation of concerns + The toggle is not a property of an individual widget; it is a rendering + strategy for the entire slot. A layout makes this explicit. With a wrapping + operation, the toggle logic is bolted onto a specific widget ID, creating a + hidden dependency between the wrapper and the slot's internal structure. + +Explicit widget identity through standard hooks + With ``useWidgets()`` exposing identity metadata, layouts can distinguish + widgets through a documented, type-safe API. The slot's children are + available as the ``defaultContent`` widget, and plugin-added widgets are + identified by their declared IDs. + +Composability + If two plugins both want to affect a slot's rendering strategy, two ``Wrap`` + operations on the same widget create nested wrappers with no coordination + mechanism. Layout replacements have a clear last-one-wins semantic, and a + layout can use ``useLayoutOptions()`` to accept configuration from multiple + plugins, enabling collaboration between them. + + +Consequences +============ + +Plugins that previously used ``Wrap`` to conditionally render or decorate slot +content will use layout replacements instead. This requires the plugin author +to write a layout component, which is slightly more code than a wrapper +function, but provides full control over rendering with access to all of the +slot's hooks and context. + +The array returned by ``useWidgets()`` will gain helper methods (``byId()``, +``withoutId()``, ``byRole()``, ``withoutRole()``) that let layouts selectively +render widgets. Because the array elements remain plain ``ReactNode`` values, +existing layouts that render ``useWidgets()`` directly will continue to work +without modification. + + +Rejected alternatives +===================== + +Widget wrapping operation +------------------------- + +Adding a ``WRAP`` widget operation that takes a target widget ID and a wrapper +component was considered and rejected. Beyond the design advantages of layouts +discussed above, a wrapping operation is fundamentally at odds with the +slot/layout/widget architecture in several ways: + +Breaks the widget rendering pipeline + Widgets are created, keyed, and enclosed in a ``WidgetProvider`` before they + are handed to the layout for rendering. A wrapping operation must either + intervene before this pipeline (losing access to the rendered node) or after + it (wrapping around the ``WidgetProvider``). In the latter case, the wrapper + sits outside the widget's context boundary, so it cannot use widget-level + hooks like ``useWidgetOptions()``. In both cases, the React ``key`` assigned + to the ``WidgetProvider`` ends up on an inner element rather than the + outermost one, breaking React's list reconciliation for any slot that renders + multiple widgets. + +Interacts poorly with other widget operations + Widget operations are sorted so that absolute operations (``APPEND``, + ``PREPEND``) execute before relative ones, guaranteeing that target widgets + exist when referenced. A wrapping operation adds another ordering + dependency: it must run after its target widget is created, but also + interacts with ``REPLACE`` (which discards and recreates the target) and + ``REMOVE`` (which deletes it). These interactions create edge cases that are + difficult to specify and surprising to plugin authors, especially when + operations originate from different plugins that are unaware of each other. + +Bypasses the layout's role + The layout is the architectural component responsible for deciding how a + slot's content is presented. A wrapping operation that modifies individual + widgets before they reach the layout undermines this responsibility, creating + a second, parallel mechanism for controlling rendering. When both are in + play, the resulting behavior depends on the interleaving of layout logic and + wrapper logic in ways that are hard to reason about. + + From 8769fb747c82fc3b10c9c7fdaf13f03e8f6e4588 Mon Sep 17 00:00:00 2001 From: "Adolfo R. Brandes" Date: Thu, 19 Mar 2026 20:53:14 -0300 Subject: [PATCH 2/5] feat: enrich useWidgets() with identity-based filtering helpers Expose byId(), withoutId(), byRole(), and withoutRole() methods on the array returned by useWidgets(), enabling layouts to selectively render widgets by identity without breaking backwards compatibility. Co-Authored-By: Claude Opus 4.6 --- runtime/slots/widget/hooks.ts | 6 +- runtime/slots/widget/types.ts | 8 ++ runtime/slots/widget/utils.test.tsx | 116 ++++++++++++++++++++++++++++ runtime/slots/widget/utils.tsx | 31 +++++++- 4 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 runtime/slots/widget/utils.test.tsx diff --git a/runtime/slots/widget/hooks.ts b/runtime/slots/widget/hooks.ts index b8e66b61..2c8b2465 100644 --- a/runtime/slots/widget/hooks.ts +++ b/runtime/slots/widget/hooks.ts @@ -1,17 +1,17 @@ import { useContext, useEffect, useState } from 'react'; import { useSlotContext, useSlotOperations } from '../hooks'; import { isSlotOperationConditionSatisfied } from '../utils'; -import { WidgetOperation } from './types'; +import { WidgetList, WidgetOperation } from './types'; import { createWidgets, isWidgetAbsoluteOperation, isWidgetOperation, isWidgetOptionsOperation, isWidgetRelativeOperation } from './utils'; import WidgetContext from './WidgetContext'; -export function useWidgets() { +export function useWidgets(): WidgetList { const { id, ...props } = useSlotContext(); delete props.children; return useWidgetsForId(id, props); } -export function useWidgetsForId(id: string, componentProps?: Record) { +export function useWidgetsForId(id: string, componentProps?: Record): WidgetList { const operations = useSortedWidgetOperations(id); return createWidgets(operations, componentProps); } diff --git a/runtime/slots/widget/types.ts b/runtime/slots/widget/types.ts index f6bc5554..b03b9a31 100644 --- a/runtime/slots/widget/types.ts +++ b/runtime/slots/widget/types.ts @@ -130,5 +130,13 @@ export type WidgetOperation = WidgetAbsoluteOperation | WidgetRelativeOperation; */ export interface IdentifiedWidget { id: string, + role?: string, node: ReactNode, } + +export interface WidgetList extends Array { + byId(id: string): ReactNode[], + withoutId(id: string): ReactNode[], + byRole(role: string): ReactNode[], + withoutRole(role: string): ReactNode[], +} diff --git a/runtime/slots/widget/utils.test.tsx b/runtime/slots/widget/utils.test.tsx new file mode 100644 index 00000000..73837280 --- /dev/null +++ b/runtime/slots/widget/utils.test.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { WidgetOperationTypes } from './types'; +import { createWidgets } from './utils'; + +// Mock WidgetProvider to just render children, avoiding context dependencies. +jest.mock('./WidgetProvider', () => ({ + __esModule: true, + default: ({ children }: { children: React.ReactNode }) => <>{children}, +})); + +// Mock condition checking to always pass. +jest.mock('../utils', () => ({ + isSlotOperationConditionSatisfied: () => true, +})); + +const slotId = 'test-slot'; + +function makeAppendOp(id: string, label: string, role?: string) { + return { + slotId, + id, + role, + op: WidgetOperationTypes.APPEND as const, + element:
{label}
, + }; +} + +describe('createWidgets', () => { + it('returns a renderable ReactNode array', () => { + const widgets = createWidgets([ + makeAppendOp('w1', 'One'), + makeAppendOp('w2', 'Two'), + ]); + + expect(widgets).toHaveLength(2); + expect(Array.isArray(widgets)).toBe(true); + }); + + describe('byId', () => { + it('returns only widgets matching the given ID', () => { + const widgets = createWidgets([ + makeAppendOp('w1', 'One'), + makeAppendOp('w2', 'Two'), + makeAppendOp('w3', 'Three'), + ]); + + const result = widgets.byId('w2'); + expect(result).toHaveLength(1); + }); + + it('returns empty array when no widgets match', () => { + const widgets = createWidgets([ + makeAppendOp('w1', 'One'), + ]); + + expect(widgets.byId('nonexistent')).toHaveLength(0); + }); + }); + + describe('withoutId', () => { + it('returns all widgets except the given ID', () => { + const widgets = createWidgets([ + makeAppendOp('w1', 'One'), + makeAppendOp('w2', 'Two'), + makeAppendOp('w3', 'Three'), + ]); + + const result = widgets.withoutId('w2'); + expect(result).toHaveLength(2); + }); + }); + + describe('byRole', () => { + it('returns only widgets matching the given role', () => { + const widgets = createWidgets([ + makeAppendOp('w1', 'One', 'sidebar'), + makeAppendOp('w2', 'Two', 'main'), + makeAppendOp('w3', 'Three', 'sidebar'), + ]); + + const result = widgets.byRole('sidebar'); + expect(result).toHaveLength(2); + }); + + it('does not include widgets without a role', () => { + const widgets = createWidgets([ + makeAppendOp('w1', 'One', 'sidebar'), + makeAppendOp('w2', 'Two'), + ]); + + expect(widgets.byRole('sidebar')).toHaveLength(1); + }); + }); + + describe('withoutRole', () => { + it('returns all widgets except those with the given role', () => { + const widgets = createWidgets([ + makeAppendOp('w1', 'One', 'sidebar'), + makeAppendOp('w2', 'Two', 'main'), + makeAppendOp('w3', 'Three', 'sidebar'), + ]); + + const result = widgets.withoutRole('sidebar'); + expect(result).toHaveLength(1); + }); + + it('includes widgets without a role', () => { + const widgets = createWidgets([ + makeAppendOp('w1', 'One', 'sidebar'), + makeAppendOp('w2', 'Two'), + ]); + + expect(widgets.withoutRole('sidebar')).toHaveLength(1); + }); + }); +}); diff --git a/runtime/slots/widget/utils.tsx b/runtime/slots/widget/utils.tsx index e4d34758..0c64bf18 100644 --- a/runtime/slots/widget/utils.tsx +++ b/runtime/slots/widget/utils.tsx @@ -2,7 +2,7 @@ import { ReactNode } from 'react'; import { SlotOperation } from '../types'; import { isSlotOperationConditionSatisfied } from '../utils'; import { IFrameWidget } from './iframe'; -import { IdentifiedWidget, WidgetAbsoluteOperation, WidgetAppendOperation, WidgetComponentProps, WidgetElementProps, WidgetIdentityProps, WidgetIFrameProps, WidgetInsertAfterOperation, WidgetInsertBeforeOperation, WidgetOperation, WidgetOperationTypes, WidgetOptionsOperation, WidgetPrependOperation, WidgetRemoveOperation, WidgetRendererOperation, WidgetRendererProps, WidgetReplaceOperation } from './types'; +import { IdentifiedWidget, WidgetAbsoluteOperation, WidgetAppendOperation, WidgetComponentProps, WidgetElementProps, WidgetIdentityProps, WidgetIFrameProps, WidgetInsertAfterOperation, WidgetInsertBeforeOperation, WidgetList, WidgetOperation, WidgetOperationTypes, WidgetOptionsOperation, WidgetPrependOperation, WidgetRemoveOperation, WidgetRendererOperation, WidgetRendererProps, WidgetReplaceOperation } from './types'; import WidgetProvider from './WidgetProvider'; export function isWidgetOperation(operation: SlotOperation): operation is WidgetOperation { @@ -117,6 +117,7 @@ function createIdentifiedWidget(operation: WidgetRendererOperation, componentPro return { id, + role: operation.role, node: ( {widget} @@ -175,7 +176,30 @@ function removeWidget(operation: WidgetRemoveOperation, widgets: IdentifiedWidge } } -export function createWidgets(operations: WidgetOperation[], componentProps?: Record) { +function createWidgetList(identifiedWidgets: IdentifiedWidget[]): WidgetList { + const nodes = identifiedWidgets.map(widget => widget.node); + const widgetList = nodes as WidgetList; + + widgetList.byId = (id: string): ReactNode[] => { + return identifiedWidgets.filter(w => w.id === id).map(w => w.node); + }; + + widgetList.withoutId = (id: string): ReactNode[] => { + return identifiedWidgets.filter(w => w.id !== id).map(w => w.node); + }; + + widgetList.byRole = (role: string): ReactNode[] => { + return identifiedWidgets.filter(w => w.role === role).map(w => w.node); + }; + + widgetList.withoutRole = (role: string): ReactNode[] => { + return identifiedWidgets.filter(w => w.role !== role).map(w => w.node); + }; + + return widgetList; +} + +export function createWidgets(operations: WidgetOperation[], componentProps?: Record): WidgetList { const identifiedWidgets: IdentifiedWidget[] = []; for (const operation of operations) { @@ -196,6 +220,5 @@ export function createWidgets(operations: WidgetOperation[], componentProps?: Re } } - // Remove the 'id' metadata and return just the nodes. - return identifiedWidgets.map(widget => widget.node); + return createWidgetList(identifiedWidgets); } From 9fa57113127f0a8af02811764eaf028d61f4639b Mon Sep 17 00:00:00 2001 From: "Adolfo R. Brandes" Date: Thu, 19 Mar 2026 21:14:38 -0300 Subject: [PATCH 3/5] feat: add widget filtering by role to slot showcase Add a ToggleByRoleLayout demo that uses the new widgets.byRole() helper to toggle between all widgets and only those with a highlighted role. Also fix two options values in the showcase that were passing JSX elements instead of plain strings, causing the options to be ignored by the type checks in their respective layouts. Remove outdated wrapping reference from the showcase intro text. Co-Authored-By: Claude Opus 4.6 --- shell/dev/slotShowcase/SlotShowcasePage.tsx | 11 ++++--- shell/dev/slotShowcase/ToggleByRoleLayout.tsx | 20 ++++++++++++ shell/dev/slotShowcase/app.tsx | 32 +++++++++++++++++-- 3 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 shell/dev/slotShowcase/ToggleByRoleLayout.tsx diff --git a/shell/dev/slotShowcase/SlotShowcasePage.tsx b/shell/dev/slotShowcase/SlotShowcasePage.tsx index ffd7721e..5355f1d0 100644 --- a/shell/dev/slotShowcase/SlotShowcasePage.tsx +++ b/shell/dev/slotShowcase/SlotShowcasePage.tsx @@ -1,17 +1,14 @@ import { Slot } from '../../../runtime'; import HorizontalSlotLayout from './HorizontalSlotLayout'; import LayoutWithOptions from './LayoutWithOptions'; +import ToggleByRoleLayout from './ToggleByRoleLayout'; export default function SlotShowcasePage() { return (

Slot Showcase

-

As a best practice, widgets should pass additional props (...props) to their rendered HTMLElement. This accomplishes two things:

-
    -
  • It allows custom layouts to add className and style props as necessary for the layout.
  • -
  • It allows widgets to be effectively "wrapped" by a parent component to alter their behavior.
  • -
+

As a best practice, widgets should pass additional props (...props) to their rendered HTMLElement. This allows custom layouts to add className and style props as necessary for the layout.

Simple slot with default layout

This slot has no opinionated layout, it just renders its children.

@@ -61,6 +58,10 @@ export default function SlotShowcasePage() {

Slot with widget with options.

Both widgets accept options. The first shows the default title, the second shows it set to "Bar"

+ +

Slot with widget filtering by role

+

This slot has four widgets, two of which have a "highlighted" role. The layout uses widgets.byRole() to toggle between showing all widgets and only the highlighted ones.

+
); } diff --git a/shell/dev/slotShowcase/ToggleByRoleLayout.tsx b/shell/dev/slotShowcase/ToggleByRoleLayout.tsx new file mode 100644 index 00000000..5f977889 --- /dev/null +++ b/shell/dev/slotShowcase/ToggleByRoleLayout.tsx @@ -0,0 +1,20 @@ +import { useState } from 'react'; +import { useWidgets } from '../../../runtime'; + +const highlitedRole = 'org.openedx.frontend.role.slotShowcase.highlighted'; + +export default function ToggleByRoleLayout() { + const widgets = useWidgets(); + const [showHighlighted, setShowHighlighted] = useState(false); + + return ( +
+ +
+ {showHighlighted ? widgets.byRole(highlitedRole) : widgets} +
+
+ ); +} diff --git a/shell/dev/slotShowcase/app.tsx b/shell/dev/slotShowcase/app.tsx index 83bf3d7a..729232e0 100644 --- a/shell/dev/slotShowcase/app.tsx +++ b/shell/dev/slotShowcase/app.tsx @@ -135,7 +135,7 @@ const app: App = { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseLayoutWithOptions', op: LayoutOperationTypes.OPTIONS, options: { - title: (), + title: 'Bar', } }, { @@ -301,10 +301,38 @@ const app: App = { relatedId: 'org.openedx.frontend.widget.slotShowcase.widgetOptionsChild2', op: WidgetOperationTypes.OPTIONS, options: { - title: (<Title title="Bar" op="WidgetOperationTypes.OPTIONS" />), + title: 'Bar', } }, + // Widget filtering by role + { + slotId: 'org.openedx.frontend.slot.dev.slotShowcaseFilterByRole', + id: 'org.openedx.frontend.widget.slotShowcase.filterChild1', + op: WidgetOperationTypes.APPEND, + element: (<Child title="Child One" />) + }, + { + slotId: 'org.openedx.frontend.slot.dev.slotShowcaseFilterByRole', + id: 'org.openedx.frontend.widget.slotShowcase.filterChild2', + role: 'org.openedx.frontend.role.slotShowcase.highlighted', + op: WidgetOperationTypes.APPEND, + element: (<Child title="Child Two (highlighted)" />) + }, + { + slotId: 'org.openedx.frontend.slot.dev.slotShowcaseFilterByRole', + id: 'org.openedx.frontend.widget.slotShowcase.filterChild3', + op: WidgetOperationTypes.APPEND, + element: (<Child title="Child Three" />) + }, + { + slotId: 'org.openedx.frontend.slot.dev.slotShowcaseFilterByRole', + id: 'org.openedx.frontend.widget.slotShowcase.filterChild4', + role: 'org.openedx.frontend.role.slotShowcase.highlighted', + op: WidgetOperationTypes.APPEND, + element: (<Child title="Child Four (highlighted)" />) + }, + // Header { slotId: 'org.openedx.frontend.slot.header.primaryLinks.v1', From 02962718b1e42df4f147728f96767c91f7809f34 Mon Sep 17 00:00:00 2001 From: "Adolfo R. Brandes" <adolfo@axim.org> Date: Fri, 20 Mar 2026 11:52:34 -0300 Subject: [PATCH 4/5] feat: expose identified widgets array on WidgetList Give slot layout authors direct access to the underlying IdentifiedWidget[] so they can apply arbitrary filtering, sorting, grouping, or any other operation beyond the built-in helpers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- runtime/slots/widget/types.ts | 1 + runtime/slots/widget/utils.test.tsx | 21 +++++++++++++++++++++ runtime/slots/widget/utils.tsx | 2 ++ 3 files changed, 24 insertions(+) diff --git a/runtime/slots/widget/types.ts b/runtime/slots/widget/types.ts index b03b9a31..9e496b38 100644 --- a/runtime/slots/widget/types.ts +++ b/runtime/slots/widget/types.ts @@ -135,6 +135,7 @@ export interface IdentifiedWidget { } export interface WidgetList extends Array<ReactNode> { + identified: IdentifiedWidget[], byId(id: string): ReactNode[], withoutId(id: string): ReactNode[], byRole(role: string): ReactNode[], diff --git a/runtime/slots/widget/utils.test.tsx b/runtime/slots/widget/utils.test.tsx index 73837280..be0de4dc 100644 --- a/runtime/slots/widget/utils.test.tsx +++ b/runtime/slots/widget/utils.test.tsx @@ -92,6 +92,27 @@ describe('createWidgets', () => { }); }); + describe('identified', () => { + it('exposes the underlying IdentifiedWidget array', () => { + const widgets = createWidgets([ + makeAppendOp('w1', 'One', 'sidebar'), + makeAppendOp('w2', 'Two', 'main'), + makeAppendOp('w3', 'Three'), + ]); + + expect(widgets.identified).toHaveLength(3); + expect(widgets.identified[0]).toEqual( + expect.objectContaining({ id: 'w1', role: 'sidebar' }) + ); + expect(widgets.identified[1]).toEqual( + expect.objectContaining({ id: 'w2', role: 'main' }) + ); + expect(widgets.identified[2]).toEqual( + expect.objectContaining({ id: 'w3', role: undefined }) + ); + }); + }); + describe('withoutRole', () => { it('returns all widgets except those with the given role', () => { const widgets = createWidgets([ diff --git a/runtime/slots/widget/utils.tsx b/runtime/slots/widget/utils.tsx index 0c64bf18..eaaf13fc 100644 --- a/runtime/slots/widget/utils.tsx +++ b/runtime/slots/widget/utils.tsx @@ -180,6 +180,8 @@ function createWidgetList(identifiedWidgets: IdentifiedWidget[]): WidgetList { const nodes = identifiedWidgets.map(widget => widget.node); const widgetList = nodes as WidgetList; + widgetList.identified = identifiedWidgets; + widgetList.byId = (id: string): ReactNode[] => { return identifiedWidgets.filter(w => w.id === id).map(w => w.node); }; From dd5f15a68e5b642a84aa3e5896056959899d504e Mon Sep 17 00:00:00 2001 From: "Adolfo R. Brandes" <adolfo@axim.org> Date: Fri, 20 Mar 2026 12:31:22 -0300 Subject: [PATCH 5/5] feat: spruce up slot showcase and fix dev home links Add visual styling to the showcase page and correct route role lookups in the dev home to use reverse-domain notation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- shell/dev/devHome/HomePage.tsx | 6 +- .../dev/slotShowcase/HorizontalSlotLayout.tsx | 5 +- shell/dev/slotShowcase/LayoutWithOptions.tsx | 2 +- shell/dev/slotShowcase/SlotShowcase.scss | 50 ++++++ shell/dev/slotShowcase/SlotShowcasePage.tsx | 156 +++++++++++++----- shell/dev/slotShowcase/ToggleByRoleLayout.tsx | 5 +- shell/dev/slotShowcase/WidgetWithOptions.tsx | 2 +- shell/dev/slotShowcase/app.tsx | 87 +++++----- 8 files changed, 211 insertions(+), 102 deletions(-) create mode 100644 shell/dev/slotShowcase/SlotShowcase.scss diff --git a/shell/dev/devHome/HomePage.tsx b/shell/dev/devHome/HomePage.tsx index e4643b3f..541ddf58 100644 --- a/shell/dev/devHome/HomePage.tsx +++ b/shell/dev/devHome/HomePage.tsx @@ -4,9 +4,9 @@ import { getUrlByRouteRole } from '../../../runtime/routing'; import messages from './messages'; export default function HomePage() { - const coursewareUrl = getUrlByRouteRole('courseware'); - const dashboardUrl = getUrlByRouteRole('learnerDashboard'); - const slotShowcaseUrl = getUrlByRouteRole('slotShowcase'); + const coursewareUrl = getUrlByRouteRole('org.openedx.frontend.role.courseware'); + const dashboardUrl = getUrlByRouteRole('org.openedx.frontend.role.dashboard'); + const slotShowcaseUrl = getUrlByRouteRole('org.openedx.frontend.role.slotShowcase'); const intl = useIntl(); return ( diff --git a/shell/dev/slotShowcase/HorizontalSlotLayout.tsx b/shell/dev/slotShowcase/HorizontalSlotLayout.tsx index 19f1c15f..fc0af58a 100644 --- a/shell/dev/slotShowcase/HorizontalSlotLayout.tsx +++ b/shell/dev/slotShowcase/HorizontalSlotLayout.tsx @@ -1,11 +1,12 @@ +import { Stack } from '@openedx/paragon'; import { useWidgets } from '../../../runtime'; export default function HorizontalSlotLayout() { const widgets = useWidgets(); return ( - <div className="d-flex gap-3"> + <Stack direction="horizontal" gap={3}> {widgets} - </div> + </Stack> ); } diff --git a/shell/dev/slotShowcase/LayoutWithOptions.tsx b/shell/dev/slotShowcase/LayoutWithOptions.tsx index 3d1df01a..c447ea58 100644 --- a/shell/dev/slotShowcase/LayoutWithOptions.tsx +++ b/shell/dev/slotShowcase/LayoutWithOptions.tsx @@ -8,7 +8,7 @@ export default function LayoutWithOptions() { return ( <> - <div>Layout Title: <strong>{title}</strong></div> + <div className="showcase-layout-title">Layout Title: {title}</div> <div> {widgets} </div> diff --git a/shell/dev/slotShowcase/SlotShowcase.scss b/shell/dev/slotShowcase/SlotShowcase.scss new file mode 100644 index 00000000..b5df7c0c --- /dev/null +++ b/shell/dev/slotShowcase/SlotShowcase.scss @@ -0,0 +1,50 @@ +// Inline code +.showcase-page code { + color: var(--pgn-color-dark-400); +} + +// Page grid +.showcase-page { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; +} + +.showcase-full-width { + grid-column: 1 / -1; +} + +.showcase-divider { + grid-column: 1 / -1; + border-bottom: 2px solid var(--pgn-color-primary-500); + padding-bottom: 0.25rem; +} + +// Slot container (dashed boundary via Card) +.showcase-slot { + border: 2px dashed var(--pgn-color-primary-300); + background-color: var(--pgn-color-primary-100); +} + +// Widget card +.showcase-widget { + background-color: var(--pgn-color-light-300); + border-left: 3px solid var(--pgn-color-primary-500); + border-radius: 0.25rem; + padding: 0.5rem; + margin: 0.5rem; +} + +// Highlighted widget variant +.showcase-widget-highlighted { + background-color: var(--pgn-color-warning-100); + border-left: 3px solid var(--pgn-color-warning-300); +} + +// Layout title +.showcase-layout-title { + font-size: 1.1rem; + font-weight: 600; + color: var(--pgn-color-primary-500); + margin-bottom: 0.5rem; +} diff --git a/shell/dev/slotShowcase/SlotShowcasePage.tsx b/shell/dev/slotShowcase/SlotShowcasePage.tsx index 5355f1d0..268acd96 100644 --- a/shell/dev/slotShowcase/SlotShowcasePage.tsx +++ b/shell/dev/slotShowcase/SlotShowcasePage.tsx @@ -1,67 +1,135 @@ +import { ReactNode } from 'react'; +import { Card, Container } from '@openedx/paragon'; import { Slot } from '../../../runtime'; import HorizontalSlotLayout from './HorizontalSlotLayout'; import LayoutWithOptions from './LayoutWithOptions'; import ToggleByRoleLayout from './ToggleByRoleLayout'; +import './SlotShowcase.scss'; + +function SlotContainer({ children }: { children: ReactNode }) { + return ( + <Card className="showcase-slot"> + <Card.Body> + {children} + </Card.Body> + </Card> + ); +} + +function Section({ title, children }: { title: string, children: ReactNode }) { + return ( + <div> + <h3 className="text-primary-500">{title}</h3> + {children} + </div> + ); +} export default function SlotShowcasePage() { return ( - <div className="p-3"> - <h1>Slot Showcase</h1> + <Container size="xl" className="showcase-page py-4"> + <div className="showcase-full-width"> + <h1>Slot Showcase</h1> + <p>As a best practice, widgets should pass additional props (<code>...props</code>) to their rendered HTMLElement. This allows custom layouts to add <code>className</code> and <code>style</code> props as necessary for the layout.</p> + </div> - <p>As a best practice, widgets should pass additional props (<code>...props</code>) to their rendered HTMLElement. This allows custom layouts to add <code>className</code> and <code>style</code> props as necessary for the layout.</p> + <Section title="Simple slot with default layout"> + <p>This slot has no opinionated layout, it just renders its children.</p> + <SlotContainer> + <Slot id="org.openedx.frontend.slot.dev.slotShowcaseSimple" /> + </SlotContainer> + </Section> - <h3>Simple slot with default layout</h3> - <p>This slot has no opinionated layout, it just renders its children.</p> - <Slot id="org.openedx.frontend.slot.dev.slotShowcaseSimple" /> + <Section title="Simple slot with default content and props"> + <p>This slot has default content, and it exposes a slot prop to widgets.</p> + <SlotContainer> + <Slot id="org.openedx.frontend.slot.dev.slotShowcaseSimpleWithDefaultContent" aSlotProp="hello!"> + <div className="showcase-widget">Look, I'm default content!</div> + </Slot> + </SlotContainer> + </Section> - <h3>Simple slot with default content and props</h3> - <p>This slot has default content, and it exposes a slot prop to widgets.</p> - <Slot id="org.openedx.frontend.slot.dev.slotShowcaseSimpleWithDefaultContent" aSlotProp="hello!"> - <div>Look, I'm default content!</div> - </Slot> + <h2 className="showcase-divider">UI Layout Operations</h2> - <h2>UI Layout Operations</h2> + <Section title="Slot with custom layout (component)"> + <p>This slot uses a horizontal flexbox layout from a component.</p> + <SlotContainer> + <Slot id="org.openedx.frontend.slot.dev.slotShowcaseCustom" layout={HorizontalSlotLayout} /> + </SlotContainer> + </Section> - <h3>Slot with custom layout</h3> - <p>This slot uses a horizontal flexbox layout from a component.</p> - <Slot id="org.openedx.frontend.slot.dev.slotShowcaseCustom" layout={HorizontalSlotLayout} /> - <p>This slot uses a horizontal flexbox layout from a JSX element.</p> - <Slot id="org.openedx.frontend.slot.dev.slotShowcaseCustom" layout={<HorizontalSlotLayout />} /> + <Section title="Slot with custom layout (element)"> + <p>This slot uses a horizontal flexbox layout from a JSX element.</p> + <SlotContainer> + <Slot id="org.openedx.frontend.slot.dev.slotShowcaseCustom" layout={<HorizontalSlotLayout />} /> + </SlotContainer> + </Section> - <h3>Slot with override custom layout</h3> - <p>This slot uses a horizontal flexbox layout, but it was added by a layout replace operation.</p> - <Slot id="org.openedx.frontend.slot.dev.slotShowcaseCustomConfig" /> + <Section title="Slot with override custom layout"> + <p>This slot uses a horizontal flexbox layout, but it was added by a layout replace operation.</p> + <SlotContainer> + <Slot id="org.openedx.frontend.slot.dev.slotShowcaseCustomConfig" /> + </SlotContainer> + </Section> - <h3>Slot with layout options</h3> - <p>These slots use a custom layout that takes options. The first shows the default title, the second shows it set to "Bar"</p> - <Slot id="org.openedx.frontend.slot.dev.slotShowcaseLayoutWithOptionsDefault" layout={LayoutWithOptions} /> - <Slot id="org.openedx.frontend.slot.dev.slotShowcaseLayoutWithOptions" layout={LayoutWithOptions} /> + <Section title="Slot with layout options (default)"> + <p>This slot uses a custom layout that takes options. It shows the default title.</p> + <SlotContainer> + <Slot id="org.openedx.frontend.slot.dev.slotShowcaseLayoutWithOptionsDefault" layout={LayoutWithOptions} /> + </SlotContainer> + </Section> - <h2>UI Widget Operations</h2> + <Section title="Slot with layout options (configured)"> + <p>Same layout, but the title option is set to "Bar".</p> + <SlotContainer> + <Slot id="org.openedx.frontend.slot.dev.slotShowcaseLayoutWithOptions" layout={LayoutWithOptions} /> + </SlotContainer> + </Section> - <h3>Slot with prepended element</h3> - <p>This slot has a prepended element (and two appended elements).</p> - <Slot id="org.openedx.frontend.slot.dev.slotShowcasePrepending" /> + <Section title="Slot with widget filtering by role"> + <p>This slot has four widgets, two with a "highlighted" role. The layout uses <code>widgets.byRole()</code> to toggle between all and highlighted.</p> + <SlotContainer> + <Slot id="org.openedx.frontend.slot.dev.slotShowcaseFilterByRole" layout={ToggleByRoleLayout} /> + </SlotContainer> + </Section> - <h3>Slot with inserted elements</h3> - <p>This slot has elements inserted before and after the second element. Also note that the insert operations are declared <em>before</em> the related element is declared, but can still insert themselves relative to it.</p> - <Slot id="org.openedx.frontend.slot.dev.slotShowcaseInserting" /> + <h2 className="showcase-divider">UI Widget Operations</h2> - <h3>Slot with replaced element</h3> - <p>This slot has an element replacing element two.</p> - <Slot id="org.openedx.frontend.slot.dev.slotShowcaseReplacing" /> + <Section title="Slot with prepended element"> + <p>This slot has a prepended element (and two appended elements).</p> + <SlotContainer> + <Slot id="org.openedx.frontend.slot.dev.slotShowcasePrepending" /> + </SlotContainer> + </Section> - <h3>Slot with removed element</h3> - <p>This slot has removed element two (<code>WidgetOperationTypes.REMOVE</code>).</p> - <Slot id="org.openedx.frontend.slot.dev.slotShowcaseRemoving" /> + <Section title="Slot with inserted elements"> + <p>This slot has elements inserted before and after the second element. The insert operations are declared <em>before</em> the related element, but can still insert themselves relative to it.</p> + <SlotContainer> + <Slot id="org.openedx.frontend.slot.dev.slotShowcaseInserting" /> + </SlotContainer> + </Section> - <h3>Slot with widget with options.</h3> - <p>Both widgets accept options. The first shows the default title, the second shows it set to "Bar"</p> - <Slot id="org.openedx.frontend.slot.dev.slotShowcaseWidgetOptions" /> + <Section title="Slot with replaced element"> + <p>This slot has an element replacing element two.</p> + <SlotContainer> + <Slot id="org.openedx.frontend.slot.dev.slotShowcaseReplacing" /> + </SlotContainer> + </Section> - <h3>Slot with widget filtering by role</h3> - <p>This slot has four widgets, two of which have a "highlighted" role. The layout uses <code>widgets.byRole()</code> to toggle between showing all widgets and only the highlighted ones.</p> - <Slot id="org.openedx.frontend.slot.dev.slotShowcaseFilterByRole" layout={ToggleByRoleLayout} /> - </div> + <Section title="Slot with removed element"> + <p>This slot has removed element two (<code>WidgetOperationTypes.REMOVE</code>).</p> + <SlotContainer> + <Slot id="org.openedx.frontend.slot.dev.slotShowcaseRemoving" /> + </SlotContainer> + </Section> + + <Section title="Slot with widget options (default)"> + <p>This widget accepts options. It shows the default title.</p> + <SlotContainer> + <Slot id="org.openedx.frontend.slot.dev.slotShowcaseWidgetOptions" /> + </SlotContainer> + </Section> + + </Container> ); } diff --git a/shell/dev/slotShowcase/ToggleByRoleLayout.tsx b/shell/dev/slotShowcase/ToggleByRoleLayout.tsx index 5f977889..13aa77a0 100644 --- a/shell/dev/slotShowcase/ToggleByRoleLayout.tsx +++ b/shell/dev/slotShowcase/ToggleByRoleLayout.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { Button } from '@openedx/paragon'; import { useWidgets } from '../../../runtime'; const highlitedRole = 'org.openedx.frontend.role.slotShowcase.highlighted'; @@ -9,9 +10,9 @@ export default function ToggleByRoleLayout() { return ( <div> - <button type="button" className="btn btn-sm btn-outline-primary mb-2" onClick={() => setShowHighlighted(!showHighlighted)}> + <Button size="sm" variant="outline-primary" className="mb-2" onClick={() => setShowHighlighted(!showHighlighted)}> {showHighlighted ? 'Show all widgets' : 'Show only highlighted widgets'} - </button> + </Button> <div> {showHighlighted ? widgets.byRole(highlitedRole) : widgets} </div> diff --git a/shell/dev/slotShowcase/WidgetWithOptions.tsx b/shell/dev/slotShowcase/WidgetWithOptions.tsx index 2194d2bf..21e5e7a9 100644 --- a/shell/dev/slotShowcase/WidgetWithOptions.tsx +++ b/shell/dev/slotShowcase/WidgetWithOptions.tsx @@ -6,6 +6,6 @@ export default function WidgetWithOptions() { const title = typeof options.title === 'string' ? options.title : 'Foo'; return ( - <div>{title}</div> + <div className="showcase-widget">{title}</div> ); } diff --git a/shell/dev/slotShowcase/app.tsx b/shell/dev/slotShowcase/app.tsx index 729232e0..67c1dde0 100644 --- a/shell/dev/slotShowcase/app.tsx +++ b/shell/dev/slotShowcase/app.tsx @@ -6,20 +6,9 @@ import HorizontalSlotLayout from './HorizontalSlotLayout'; import SlotShowcasePage from './SlotShowcasePage'; import WidgetWithOptions from './WidgetWithOptions'; -function Title({ title, op }: { title: string, op?: string }) { +function Widget({ title, op, className, ...props }: { title: string, op?: string, className?: string } & Record<string, unknown>) { return ( - <span> - {title} - {op && ( - <>{' '}(<code>{op}</code>)</> - )} - </span> - ); -} - -function Child({ title, op }: { title: string, op?: string }) { - return ( - <div> + <div className={`showcase-widget ${className ?? ''}`} {...props}> {title} {op && ( <span>{' '}(<code>{op}</code>)</span> @@ -30,7 +19,7 @@ function Child({ title, op }: { title: string, op?: string }) { function TakesProps({ aSlotProp }: { aSlotProp: string }) { return ( - <div>And this is a slot prop that was passed down via props: <code>{aSlotProp}</code></div> + <div className="showcase-widget">And this is a slot prop that was passed down via props: <code>{aSlotProp}</code></div> ); } @@ -38,7 +27,7 @@ function TakesPropsViaContext() { const slotContext = useSlotContext(); const aSlotProp = typeof slotContext.aSlotProp === 'string' ? slotContext.aSlotProp : 'foo'; return ( - <div>And this is the same prop, but accessed via slot context: <code>{aSlotProp}</code></div> + <div className="showcase-widget">And this is the same prop, but accessed via slot context: <code>{aSlotProp}</code></div> ); } @@ -58,19 +47,19 @@ const app: App = { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseSimple', id: 'org.openedx.frontend.widget.slotShowcase.simpleChild1', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child One" />) + element: (<Widget title="Widget One" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseSimple', id: 'org.openedx.frontend.widget.slotShowcase.simpleChild2', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Two" />) + element: (<Widget title="Widget Two" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseSimple', id: 'org.openedx.frontend.widget.slotShowcase.simpleChild3', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Three" />) + element: (<Widget title="Widget Three" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseSimpleWithDefaultContent', @@ -90,19 +79,19 @@ const app: App = { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseCustom', id: 'org.openedx.frontend.widget.slotShowcase.customChild1', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child One" />) + element: (<Widget title="Widget One" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseCustom', id: 'org.openedx.frontend.widget.slotShowcase.customChild2', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Two" />) + element: (<Widget title="Widget Two" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseCustom', id: 'org.openedx.frontend.widget.slotShowcase.customChild3', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Three" />) + element: (<Widget title="Widget Three" />) }, // Override custom layout @@ -110,19 +99,19 @@ const app: App = { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseCustomConfig', id: 'org.openedx.frontend.widget.slotShowcase.customConfigChild1', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child One" />) + element: (<Widget title="Widget One" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseCustomConfig', id: 'org.openedx.frontend.widget.slotShowcase.customConfigChild2', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Two" />) + element: (<Widget title="Widget Two" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseCustomConfig', id: 'org.openedx.frontend.widget.slotShowcase.customConfigChild3', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Three" />) + element: (<Widget title="Widget Three" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseCustomConfig', @@ -142,37 +131,37 @@ const app: App = { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseLayoutWithOptions', id: 'org.openedx.frontend.widget.slotShowcase.layoutWithOptionsChild1', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child One" />) + element: (<Widget title="Widget One" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseLayoutWithOptions', id: 'org.openedx.frontend.widget.slotShowcase.layoutWithOptionsChild2', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Two" />) + element: (<Widget title="Widget Two" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseLayoutWithOptions', id: 'org.openedx.frontend.widget.slotShowcase.layoutWithOptionsChild3', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Three" />) + element: (<Widget title="Widget Three" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseLayoutWithOptionsDefault', id: 'org.openedx.frontend.widget.slotShowcase.layoutWithOptionsDefaultChild1', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child One" />) + element: (<Widget title="Widget One" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseLayoutWithOptionsDefault', id: 'org.openedx.frontend.widget.slotShowcase.layoutWithOptionsDefaultChild2', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Two" />) + element: (<Widget title="Widget Two" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseLayoutWithOptionsDefault', id: 'org.openedx.frontend.widget.slotShowcase.layoutWithOptionsDefaultChild3', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Three" />) + element: (<Widget title="Widget Three" />) }, // TODO: Override Layout @@ -182,19 +171,19 @@ const app: App = { slotId: 'org.openedx.frontend.slot.dev.slotShowcasePrepending', id: 'org.openedx.frontend.widget.slotShowcase.prependingChild1', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child One" op="WidgetOperationTypes.APPEND" />) + element: (<Widget title="Widget One" op="WidgetOperationTypes.APPEND" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcasePrepending', id: 'org.openedx.frontend.widget.slotShowcase.prependingChild2', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Two" op="WidgetOperationTypes.APPEND" />) + element: (<Widget title="Widget Two" op="WidgetOperationTypes.APPEND" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcasePrepending', id: 'org.openedx.frontend.widget.slotShowcase.prependingChild3', op: WidgetOperationTypes.PREPEND, - element: (<Child title="Child Three" op="WidgetOperationTypes.PREPEND" />) + element: (<Widget title="Widget Three" op="WidgetOperationTypes.PREPEND" />) }, // Inserting @@ -203,32 +192,32 @@ const app: App = { id: 'slot-showcase.inserting.child4', op: WidgetOperationTypes.INSERT_AFTER, relatedId: 'org.openedx.frontend.widget.slotShowcase.insertingChild2', - element: (<Child title="Child Four" op="WidgetOperationTypes.INSERT_AFTER" />) + element: (<Widget title="Widget Four" op="WidgetOperationTypes.INSERT_AFTER" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseInserting', id: 'slot-showcase.inserting.child5', op: WidgetOperationTypes.INSERT_BEFORE, relatedId: 'org.openedx.frontend.widget.slotShowcase.insertingChild2', - element: (<Child title="Child Five" op="WidgetOperationTypes.INSERT_BEFORE" />) + element: (<Widget title="Widget Five" op="WidgetOperationTypes.INSERT_BEFORE" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseInserting', id: 'org.openedx.frontend.widget.slotShowcase.insertingChild1', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child One" />) + element: (<Widget title="Widget One" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseInserting', id: 'org.openedx.frontend.widget.slotShowcase.insertingChild2', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Two" />) + element: (<Widget title="Widget Two" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseInserting', id: 'org.openedx.frontend.widget.slotShowcase.insertingChild3', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Three" />) + element: (<Widget title="Widget Three" />) }, // Replacing @@ -236,26 +225,26 @@ const app: App = { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseReplacing', id: 'org.openedx.frontend.widget.slotShowcase.replacingChild1', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child One" />) + element: (<Widget title="Widget One" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseReplacing', id: 'org.openedx.frontend.widget.slotShowcase.replacingChild2', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Two" />) + element: (<Widget title="Widget Two" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseReplacing', id: 'org.openedx.frontend.widget.slotShowcase.replacingChild3', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Three" />) + element: (<Widget title="Widget Three" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseReplacing', id: 'org.openedx.frontend.widget.slotShowcase.replacingChild4', op: WidgetOperationTypes.REPLACE, relatedId: 'org.openedx.frontend.widget.slotShowcase.replacingChild2', - element: (<Child title="Child Four" op="WidgetOperationTypes.REPLACE" />) + element: (<Widget title="Widget Four" op="WidgetOperationTypes.REPLACE" />) }, // Hiding @@ -263,19 +252,19 @@ const app: App = { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseRemoving', id: 'org.openedx.frontend.widget.slotShowcase.removingChild1', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child One" />) + element: (<Widget title="Widget One" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseRemoving', id: 'org.openedx.frontend.widget.slotShowcase.removingChild2', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Two" />) + element: (<Widget title="Widget Two" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseRemoving', id: 'org.openedx.frontend.widget.slotShowcase.removingChild3', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Three" />) + element: (<Widget title="Widget Three" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseRemoving', @@ -310,27 +299,27 @@ const app: App = { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseFilterByRole', id: 'org.openedx.frontend.widget.slotShowcase.filterChild1', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child One" />) + element: (<Widget title="Widget One" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseFilterByRole', id: 'org.openedx.frontend.widget.slotShowcase.filterChild2', role: 'org.openedx.frontend.role.slotShowcase.highlighted', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Two (highlighted)" />) + element: (<Widget title="Widget Two (highlighted)" className="showcase-widget-highlighted" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseFilterByRole', id: 'org.openedx.frontend.widget.slotShowcase.filterChild3', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Three" />) + element: (<Widget title="Widget Three" />) }, { slotId: 'org.openedx.frontend.slot.dev.slotShowcaseFilterByRole', id: 'org.openedx.frontend.widget.slotShowcase.filterChild4', role: 'org.openedx.frontend.role.slotShowcase.highlighted', op: WidgetOperationTypes.APPEND, - element: (<Child title="Child Four (highlighted)" />) + element: (<Widget title="Widget Four (highlighted)" className="showcase-widget-highlighted" />) }, // Header