Skip to content

Commit 35d27c7

Browse files
committed
Highlight active RSVP phrase (#32)
1 parent 78688b1 commit 35d27c7

6 files changed

Lines changed: 105 additions & 0 deletions

File tree

src/PrompterOne.Shared/Contracts/UiTestIds.Learn.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public static class Learn
88
public const string ContextLeft = "learn-context-left";
99
public const string ContextRight = "learn-context-right";
1010
public const string Display = "learn-display";
11+
public const string ActivePhraseHighlight = "learn-active-phrase-highlight";
1112
public const string FocusRow = "learn-focus-row";
1213
public const string LoopToggle = "learn-loop-toggle";
1314
public const string NextPhrase = "learn-next-phrase";

src/PrompterOne.Shared/Learn/Pages/LearnPage.razor

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
</div>
3434

3535
<div class="rsvp-h-row" @ref="_focusRow" data-test="@UiTestIds.Learn.FocusRow">
36+
<div class="rsvp-active-phrase-highlight"
37+
aria-hidden="true"
38+
data-test="@UiTestIds.Learn.ActivePhraseHighlight"></div>
3639
<div class="rsvp-lane rsvp-lane-left">
3740
<div class="rsvp-ctx-left" id="@UiDomIds.Learn.ContextLeft" data-test="@UiTestIds.Learn.ContextLeft">
3841
@foreach (var word in _leftContextWords)

src/PrompterOne.Shared/wwwroot/design/modules/30-rsvp.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@
104104
min-width: 0;
105105
display: flex;
106106
align-items: baseline;
107+
position: relative;
108+
z-index: 1;
107109
}
108110

109111
.rsvp-lane-left {
@@ -153,6 +155,21 @@
153155
z-index: 1;
154156
}
155157

158+
.rsvp-active-phrase-highlight {
159+
position: absolute;
160+
left: clamp(34px, 9vw, 170px);
161+
right: clamp(34px, 9vw, 170px);
162+
bottom: 24px;
163+
height: 48px;
164+
border-radius: 999px;
165+
border-bottom: 2px solid rgba(232, 213, 176, .24);
166+
background:
167+
linear-gradient(90deg, transparent 0%, rgba(232, 213, 176, .055) 18%, rgba(232, 213, 176, .085) 50%, rgba(232, 213, 176, .055) 82%, transparent 100%);
168+
box-shadow: 0 12px 36px rgba(232, 213, 176, .045);
169+
pointer-events: none;
170+
z-index: 0;
171+
}
172+
156173
/* Focus word — large, centered, with ORP highlight */
157174
.rsvp-focus {
158175
display: inline-flex;

src/PrompterOne.Shared/wwwroot/design/modules/31-rsvp-responsive.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
min-height: 112px;
1515
}
1616

17+
.rsvp-active-phrase-highlight {
18+
left: clamp(18px, 6vw, 42px);
19+
right: clamp(18px, 6vw, 42px);
20+
bottom: 18px;
21+
height: 38px;
22+
}
23+
1724
.rsvp-ctx-left,
1825
.rsvp-ctx-right {
1926
gap: 10px;

tests/PrompterOne.Web.Tests/AppShell/ScreenShellContractTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public void LearnPage_RendersCenteredRsvpSurfaceAndInitializesTimeline()
6363
{
6464
Assert.NotNull(cut.FindByTestId(UiTestIds.Learn.Page));
6565
Assert.NotNull(cut.FindByTestId(UiTestIds.Learn.Display));
66+
Assert.NotNull(cut.FindByTestId(UiTestIds.Learn.ActivePhraseHighlight));
6667
Assert.NotNull(cut.FindByTestId(UiTestIds.Learn.Word));
6768
Assert.NotNull(cut.FindByTestId(UiTestIds.Learn.WordShell));
6869
Assert.NotNull(cut.FindByTestId(UiTestIds.Learn.OrpLine));

tests/PrompterOne.Web.UITests.Reader/Learn/LearnFidelityTests.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ public sealed class LearnFidelityTests(StandaloneAppFixture fixture)
1212
private readonly record struct ContextGapMeasurement(double LeftGapPx, double RightGapPx);
1313
private readonly record struct ContextRailClipMeasurement(double LeftClipPx, double RightClipPx);
1414
private readonly record struct FocusWordSlackMeasurement(double SlackPx);
15+
private readonly record struct ActivePhraseHighlightMeasurement(
16+
double HighlightWidthPx,
17+
double RowWidthPx,
18+
double WordWidthPx,
19+
string BorderBottomStyle,
20+
string BackgroundImage);
1521
private readonly record struct OrpDeltaMeasurement(double DeltaPx);
1622
private readonly record struct VisibleContextWordGapMeasurement(double LeftWordGapPx, double RightWordGapPx);
1723

@@ -76,6 +82,38 @@ await Expect(page.GetByTestId(UiTestIds.Learn.NextPhrase))
7682
}
7783
}
7884

85+
[Test]
86+
public async Task LearnScreen_ActivePhraseHighlightMarksMoreThanTheCurrentWord()
87+
{
88+
const string scenario = "learn-active-phrase-highlight";
89+
const string step = "active-phrase-highlight";
90+
var page = await fixture.NewPageAsync(additionalContext: true);
91+
92+
try
93+
{
94+
UiScenarioArtifacts.ResetScenario(scenario);
95+
96+
await ReaderRouteDriver.OpenLearnAsync(page, BrowserTestConstants.Routes.LearnDemo);
97+
await Expect(page.GetByTestId(UiTestIds.Learn.Page))
98+
.ToBeVisibleAsync(new() { Timeout = BrowserTestConstants.Timing.ExtendedVisibleTimeoutMs });
99+
await Expect(page.GetByTestId(UiTestIds.Learn.ActivePhraseHighlight)).ToBeVisibleAsync();
100+
await WaitForLearnLayoutReadyAsync(page);
101+
102+
var measurement = await MeasureActivePhraseHighlightAsync(page);
103+
104+
await Assert.That(measurement.HighlightWidthPx > measurement.WordWidthPx * 1.45).IsTrue().Because($"Expected the RSVP active phrase highlight to span beyond only the current word, but highlight={measurement.HighlightWidthPx:0.##}px and word={measurement.WordWidthPx:0.##}px.");
105+
await Assert.That(measurement.HighlightWidthPx <= measurement.RowWidthPx).IsTrue().Because($"Expected the RSVP active phrase highlight to stay inside the focus row, but highlight={measurement.HighlightWidthPx:0.##}px and row={measurement.RowWidthPx:0.##}px.");
106+
await Assert.That(measurement.BorderBottomStyle).IsEqualTo("solid");
107+
await Assert.That(measurement.BackgroundImage).IsNotEqualTo("none");
108+
109+
await UiScenarioArtifacts.CapturePageAsync(page, scenario, step);
110+
}
111+
finally
112+
{
113+
await page.Context.CloseAsync();
114+
}
115+
}
116+
79117
[Test]
80118
public async Task LearnScreen_DemoContextRails_ShowTwoWordsPerSideWithoutRightRailClipping()
81119
{
@@ -462,6 +500,44 @@ private static Task<ContextGapMeasurement> MeasureContextGapsAsync(Microsoft.Pla
462500
right = UiTestIds.Learn.ContextRight
463501
});
464502

503+
private static Task<ActivePhraseHighlightMeasurement> MeasureActivePhraseHighlightAsync(Microsoft.Playwright.IPage page) =>
504+
page.EvaluateAsync<ActivePhraseHighlightMeasurement>(
505+
"""
506+
ids => {
507+
const highlight = document.querySelector(`[data-test="${ids.highlight}"]`);
508+
const row = document.querySelector(`[data-test="${ids.row}"]`);
509+
const word = document.querySelector(`[data-test="${ids.word}"]`);
510+
if (!highlight || !row || !word) {
511+
return {
512+
highlightWidthPx: 0,
513+
rowWidthPx: 0,
514+
wordWidthPx: 0,
515+
borderBottomStyle: '',
516+
backgroundImage: 'none'
517+
};
518+
}
519+
520+
const highlightRect = highlight.getBoundingClientRect();
521+
const rowRect = row.getBoundingClientRect();
522+
const wordRect = word.getBoundingClientRect();
523+
const style = getComputedStyle(highlight);
524+
525+
return {
526+
highlightWidthPx: highlightRect.width,
527+
rowWidthPx: rowRect.width,
528+
wordWidthPx: wordRect.width,
529+
borderBottomStyle: style.borderBottomStyle,
530+
backgroundImage: style.backgroundImage
531+
};
532+
}
533+
""",
534+
new
535+
{
536+
highlight = UiTestIds.Learn.ActivePhraseHighlight,
537+
row = UiTestIds.Learn.FocusRow,
538+
word = UiTestIds.Learn.Word
539+
});
540+
465541
private static Task<VisibleContextWordGapMeasurement> MeasureVisibleContextWordGapsAsync(Microsoft.Playwright.IPage page) =>
466542
page.EvaluateAsync<VisibleContextWordGapMeasurement>(
467543
"""

0 commit comments

Comments
 (0)