Feed tabs say what they are
Below xl every feed tab but the selected one was a lone icon with no name, for a screen reader or a finger. DTab takes an AccessibleName, which DTabs renders as the tab's label and tooltip, and FeedTabs names each tab; on a phone every tab shows its name too. The strip is marked as one that scrolls sideways (data-scroll-x), says it holds the feeds, and on a phone fades at the edge where more tabs wait (edge-fade-x, a scroll-driven animation where the browser has one); the selected tab is brought into view when it changes (window.revealActive). Explore's tabs get the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
1c9773bab0
commit
dd38002682
9 files changed
+97
-6
No files matched your search
@@ -9,6 +9,8 @@
|
||||
[Parameter] public RenderFragment ChildContent { get; set; }
|
||||
[Parameter] public bool Disabled { get; set; }
|
||||
[Parameter] public string Class { get; set; }
|
||||
/// <summary>The tab's name for a screen reader and as its tooltip, when its Header shows only an icon.</summary>
|
||||
[Parameter] public string AccessibleName { get; set; }
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
var index = i;
|
||||
var tab = tabs[i];
|
||||
<button type="button" role="tab" aria-selected="@(index == Active ? "true" : "false")" disabled="@tab.Disabled"
|
||||
aria-label="@tab.AccessibleName" title="@tab.AccessibleName"
|
||||
class="@Cx("tab", index == Active ? "tab-active" : null, tab.Disabled ? "tab-disabled" : null, tab.Class)"
|
||||
@onclick="() => Select(index)">
|
||||
@tab.Header
|
||||
|
||||
@@ -6,14 +6,17 @@
|
||||
read; the buttons on the right open the composer and, on narrow screens, the globe. *@
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<div class="flex-1 min-w-0 overflow-x-auto">
|
||||
<DTabs Active="ActiveIndex" ActiveChanged="Select" Variant="DTabsVariant.Box" Size="DSize.Sm" ContentClass="hidden" class="flex-nowrap w-max">
|
||||
@* a sideways strip on a phone, every tab named, the selected one brought into view and the edges faded where more
|
||||
tabs wait; from md only the selected tab shows its name until xl *@
|
||||
<div @ref="strip" class="flex-1 min-w-0 overflow-x-auto max-md:edge-fade-x" data-scroll-x>
|
||||
<DTabs Active="ActiveIndex" ActiveChanged="Select" Variant="DTabsVariant.Box" Size="DSize.Sm" ContentClass="hidden" class="flex-nowrap w-max"
|
||||
aria-label="@Localizer["Feeds"]">
|
||||
@foreach (var type in Types)
|
||||
{
|
||||
<DTab Class="gap-1 whitespace-nowrap">
|
||||
<DTab Class="gap-1 whitespace-nowrap" AccessibleName="@TabName(type)">
|
||||
<Header>
|
||||
<i class="@type.GetTimelineTypeIcon()" aria-hidden="true"></i>
|
||||
<span class="@(type == Filter.TimelineType ? null : "hidden xl:inline")">@TabName(type)</span>
|
||||
<span class="@(type == Filter.TimelineType ? null : "hidden max-md:inline xl:inline")">@TabName(type)</span>
|
||||
@if (type is TimelineType.Notifications && UnreadNotifications > 0)
|
||||
{
|
||||
<DBadge Size="DSize.Xs" Color="DColor.Primary" title="@Localizer["{0} unread", UnreadNotifications]" data-unread>@UnreadNotifications</DBadge>
|
||||
@@ -127,6 +130,17 @@
|
||||
|
||||
int ActiveIndex => Types.IndexOf(Filter.TimelineType);
|
||||
|
||||
ElementReference strip;
|
||||
int revealed = -1;
|
||||
|
||||
protected override void OnAfterRender(bool firstRender)
|
||||
{
|
||||
if (ActiveIndex == revealed)
|
||||
return;
|
||||
revealed = ActiveIndex;
|
||||
CascadingState.Status.RevealActive(strip);
|
||||
}
|
||||
|
||||
ViewGroup SelectedGroup => Groups.FirstOrDefault(g => g.Id == Filter.GroupId);
|
||||
|
||||
[Inject] DiscoveryService DiscoveryService { get; set; }
|
||||
|
||||
+14
-2
@@ -14,8 +14,9 @@
|
||||
<h1 class="font-bold truncate">@Localizer["Explore"]</h1>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto flex-none">
|
||||
<DTabs Active="(int)tab" ActiveChanged="SelectTab" Variant="DTabsVariant.Box" Size="DSize.Sm" ContentClass="hidden" class="flex-nowrap w-max">
|
||||
<div @ref="strip" class="overflow-x-auto flex-none max-md:edge-fade-x" data-scroll-x>
|
||||
<DTabs Active="(int)tab" ActiveChanged="SelectTab" Variant="DTabsVariant.Box" Size="DSize.Sm" ContentClass="hidden" class="flex-nowrap w-max"
|
||||
aria-label="@Localizer["Explore"]">
|
||||
<DTab><Header><i class="ion-md-pricetag"></i> @Localizer["Hashtags"]</Header></DTab>
|
||||
<DTab><Header><i class="ion-md-flame"></i> @Localizer["Popular posts"]</Header></DTab>
|
||||
<DTab><Header><i class="ion-md-link"></i> @Localizer["Shared links"]</Header></DTab>
|
||||
@@ -139,6 +140,17 @@
|
||||
</section>
|
||||
|
||||
@code {
|
||||
ElementReference strip;
|
||||
int revealed = -1;
|
||||
|
||||
protected override void OnAfterRender(bool firstRender)
|
||||
{
|
||||
if ((int)tab == revealed)
|
||||
return;
|
||||
revealed = (int)tab;
|
||||
CascadingState.Status.RevealActive(strip);
|
||||
}
|
||||
|
||||
enum ExploreTab
|
||||
{
|
||||
Hashtags,
|
||||
|
||||
@@ -1853,4 +1853,7 @@
|
||||
<data name="Every persona of this login is signed out on this device." xml:space="preserve">
|
||||
<value>Tutte le persone di questo accesso escono da questo dispositivo.</value>
|
||||
</data>
|
||||
<data name="Feeds" xml:space="preserve">
|
||||
<value>Feed</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1853,4 +1853,7 @@
|
||||
<data name="Every persona of this login is signed out on this device." xml:space="preserve">
|
||||
<value>Every persona of this login is signed out on this device.</value>
|
||||
</data>
|
||||
<data name="Feeds" xml:space="preserve">
|
||||
<value>Feeds</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,3 +1,4 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using decePubClient.Models;
|
||||
|
||||
using Microsoft.JSInterop;
|
||||
@@ -41,6 +42,9 @@ public class AppStatusService(IJSRuntime jsRuntime)
|
||||
|
||||
public bool IsMobileMedia() => JsSyncRuntime.Invoke<bool>("isMobileMedia");
|
||||
|
||||
// a sideways-scrolling strip of tabs shows its selected tab
|
||||
public void RevealActive(ElementReference strip) => JsSyncRuntime.InvokeVoid("revealActive", strip);
|
||||
|
||||
public bool CanShare() => JsSyncRuntime.Invoke<bool>("canShareStuff");
|
||||
|
||||
public async ValueTask ShareTextUrl(string title, string text, Uri uri, CancellationToken cancellationToken = default) =>
|
||||
|
||||
@@ -112,3 +112,46 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* A sideways strip fades at the edge where more of it waits: the right edge at first, both while scrolled, the left
|
||||
at the end (a scroll-driven animation where the browser has one; without, the right edge only). */
|
||||
@property --fade-start {
|
||||
syntax: "<length>";
|
||||
inherits: false;
|
||||
initial-value: 0px;
|
||||
}
|
||||
|
||||
@property --fade-end {
|
||||
syntax: "<length>";
|
||||
inherits: false;
|
||||
initial-value: 1.5rem;
|
||||
}
|
||||
|
||||
@keyframes edge-fade-x {
|
||||
from {
|
||||
--fade-start: 0px;
|
||||
--fade-end: 1.5rem;
|
||||
}
|
||||
|
||||
8% {
|
||||
--fade-start: 1.5rem;
|
||||
}
|
||||
|
||||
92% {
|
||||
--fade-end: 1.5rem;
|
||||
}
|
||||
|
||||
to {
|
||||
--fade-start: 1.5rem;
|
||||
--fade-end: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@utility edge-fade-x {
|
||||
mask-image: linear-gradient(to right, transparent, #000 var(--fade-start), #000 calc(100% - var(--fade-end)), transparent);
|
||||
|
||||
@supports (animation-timeline: scroll()) {
|
||||
animation: edge-fade-x linear both;
|
||||
animation-timeline: scroll(self inline);
|
||||
}
|
||||
}
|
||||
@@ -123,6 +123,15 @@ window.clearAppBadge = () => {
|
||||
window.logFromJs(e.message, "clearAppBadge")
|
||||
}
|
||||
}
|
||||
// the selected tab of a sideways-scrolling strip, brought into its middle
|
||||
window.revealActive = strip => {
|
||||
try {
|
||||
strip?.querySelector("[aria-selected=true]")?.scrollIntoView({ inline: "center", block: "nearest" })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
window.logFromJs(e.message, "revealActive")
|
||||
}
|
||||
}
|
||||
// a phone: narrower than Tailwind's md, or held sideways (as short as the short: variant)
|
||||
window.isMobileMedia = () => {
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user