Skip to content

Commit d5c95b5

Browse files
feat(passage): add heading level handling PIE-150
1 parent 187aac8 commit d5c95b5

2 files changed

Lines changed: 67 additions & 20 deletions

File tree

packages/passage/src/index.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,31 @@ import { createRoot } from 'react-dom/client';
55

66
import StimulusTabs from './stimulus-tabs';
77

8+
function getBaseHeadingLevel(element) {
9+
const player =
10+
element.closest('pie-player') ||
11+
element.closest('pie-item-player');
12+
13+
if (player) {
14+
let raw = player.baseHeadingLevel;
15+
16+
// fallback in case someone sets via HTML attribute manually
17+
if (raw == null) {
18+
raw =
19+
player.getAttribute('base-heading-level') ??
20+
player.getAttribute('baseheadinglevel');
21+
}
22+
23+
const playerLevel = parseInt(raw, 10);
24+
25+
if (Number.isFinite(playerLevel) && playerLevel >= 1 && playerLevel <= 6) {
26+
return playerLevel;
27+
}
28+
}
29+
30+
return undefined;
31+
}
32+
833
export default class PiePassage extends HTMLElement {
934
constructor() {
1035
super();
@@ -15,6 +40,7 @@ export default class PiePassage extends HTMLElement {
1540
this._root = null;
1641
this._mathObserver = null;
1742
this._mathRenderPending = false;
43+
this._playerObserver = null;
1844
}
1945

2046
setLangAttribute() {
@@ -70,6 +96,7 @@ export default class PiePassage extends HTMLElement {
7096
this.setAttribute('aria-label', 'Passage');
7197
this.setAttribute('role', 'region');
7298
this._initMathObserver();
99+
this._initPlayerObserver();
73100
this._render();
74101
}
75102

@@ -84,6 +111,8 @@ export default class PiePassage extends HTMLElement {
84111

85112
const elem = React.createElement(StimulusTabs, {
86113
tabs: passagesTabs,
114+
model: this._model,
115+
baseHeadingLevel: getBaseHeadingLevel(this),
87116
});
88117

89118
if (!this._root) {
@@ -95,8 +124,26 @@ export default class PiePassage extends HTMLElement {
95124
}
96125
}
97126

127+
_initPlayerObserver() {
128+
const player = this.closest('pie-player') || this.closest('pie-item-player');
129+
if (!player) return;
130+
131+
this._playerObserver = new MutationObserver(() => {
132+
this._render();
133+
});
134+
this._playerObserver.observe(player, { attributes: true, attributeFilter: ['base-heading-level'] });
135+
}
136+
137+
_disconnectPlayerObserver() {
138+
if (this._playerObserver) {
139+
this._playerObserver.disconnect();
140+
this._playerObserver = null;
141+
}
142+
}
143+
98144
disconnectedCallback() {
99145
this._disconnectMathObserver();
146+
this._disconnectPlayerObserver();
100147
if (this._root) {
101148
this._root.unmount();
102149
}

packages/passage/src/stimulus-tabs.jsx

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
33
import Tabs from '@mui/material/Tabs';
44
import Tab from '@mui/material/Tab';
55
import { styled } from '@mui/material/styles';
6-
import { Collapsible, color, PreviewPrompt, Purpose, UiLayout } from '@pie-lib/render-ui';
6+
import { Collapsible, color, PreviewPrompt, Purpose, UiLayout, transformDataHeadings } from '@pie-lib/render-ui';
77

88
const PassagesContainer = styled('div')({
99
flexGrow: 1,
@@ -172,24 +172,27 @@ class StimulusTabs extends React.Component {
172172
}
173173

174174
renderTab(tab, disabledTabs) {
175+
const { baseHeadingLevel } = this.props;
176+
const clampedLevel = !!baseHeadingLevel ? Math.min(6, Math.max(1, baseHeadingLevel)) : undefined;
177+
const TitleTag = !!baseHeadingLevel ? `h${clampedLevel}` : 'h2'; // default to h2 if no base level is provided - this was the previous behavior
178+
const textLevel = !!baseHeadingLevel ? Math.min(6, Math.max(1, clampedLevel + 1)) : undefined; // promote text headings one level above title
179+
175180
return (
176181
<Passage key={tab.id} id={`tabpanel-${tab.id}`} role="tabpanel" aria-labelledby={`button-${tab.id}`}>
177182
{this.renderInstructions(tab.teacherInstructions, disabledTabs)}
178183

179-
{(tab.title || tab.subtitle) && (
180-
<h2>
181-
{tab.title && (
182-
<Purpose purpose="passage-title">
183-
<PassageTitle dangerouslySetInnerHTML={{ __html: this.parsedText(tab.title) }}/>
184-
</Purpose>
185-
)}
186-
{tab.subtitle && (
187-
<Purpose purpose="passage-subtitle">
188-
<PassageSubtitle dangerouslySetInnerHTML={{ __html: this.parsedText(tab.subtitle) }}
189-
/>
190-
</Purpose>
191-
)}
192-
</h2>
184+
{tab.title && (
185+
<Purpose purpose="passage-title">
186+
<TitleTag>
187+
<PassageTitle dangerouslySetInnerHTML={{ __html: this.parsedText(tab.title) }} />
188+
</TitleTag>
189+
</Purpose>
190+
)}
191+
192+
{tab.subtitle && (
193+
<Purpose purpose="passage-subtitle">
194+
<PassageSubtitle dangerouslySetInnerHTML={{ __html: this.parsedText(tab.subtitle) }} />
195+
</Purpose>
193196
)}
194197

195198
{tab.author && (
@@ -200,11 +203,7 @@ class StimulusTabs extends React.Component {
200203

201204
{tab.text && (
202205
<Purpose purpose="passage-text">
203-
<div
204-
key={tab.id}
205-
className="text"
206-
dangerouslySetInnerHTML={{ __html: this.parsedText(tab.text) }}
207-
/>
206+
<div key={tab.id} className="text" dangerouslySetInnerHTML={{ __html: !!baseHeadingLevel ? transformDataHeadings(tab.text, textLevel) : tab.text }} />
208207
</Purpose>
209208
)}
210209
</Passage>
@@ -283,6 +282,7 @@ StimulusTabs.propTypes = {
283282
).isRequired,
284283
disabledTabs: PropTypes.bool,
285284
model: PropTypes.object,
285+
baseHeadingLevel: PropTypes.number,
286286
};
287287

288288
export default StimulusTabs;

0 commit comments

Comments
 (0)