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 RenderFragment ChildContent { get; set; }
|
||||||
[Parameter] public bool Disabled { get; set; }
|
[Parameter] public bool Disabled { get; set; }
|
||||||
[Parameter] public string Class { 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()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
var index = i;
|
var index = i;
|
||||||
var tab = tabs[i];
|
var tab = tabs[i];
|
||||||
<button type="button" role="tab" aria-selected="@(index == Active ? "true" : "false")" disabled="@tab.Disabled"
|
<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)"
|
class="@Cx("tab", index == Active ? "tab-active" : null, tab.Disabled ? "tab-disabled" : null, tab.Class)"
|
||||||
@onclick="() => Select(index)">
|
@onclick="() => Select(index)">
|
||||||
@tab.Header
|
@tab.Header
|
||||||
|
|||||||
@@ -6,14 +6,17 @@
|
|||||||
read; the buttons on the right open the composer and, on narrow screens, the globe. *@
|
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 flex-col gap-2">
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
<div class="flex-1 min-w-0 overflow-x-auto">
|
@* a sideways strip on a phone, every tab named, the selected one brought into view and the edges faded where more
|
||||||
<DTabs Active="ActiveIndex" ActiveChanged="Select" Variant="DTabsVariant.Box" Size="DSize.Sm" ContentClass="hidden" class="flex-nowrap w-max">
|
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)
|
@foreach (var type in Types)
|
||||||
{
|
{
|
||||||
<DTab Class="gap-1 whitespace-nowrap">
|
<DTab Class="gap-1 whitespace-nowrap" AccessibleName="@TabName(type)">
|
||||||
<Header>
|
<Header>
|
||||||
<i class="@type.GetTimelineTypeIcon()" aria-hidden="true"></i>
|
<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)
|
@if (type is TimelineType.Notifications && UnreadNotifications > 0)
|
||||||
{
|
{
|
||||||
<DBadge Size="DSize.Xs" Color="DColor.Primary" title="@Localizer["{0} unread", UnreadNotifications]" data-unread>@UnreadNotifications</DBadge>
|
<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);
|
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);
|
ViewGroup SelectedGroup => Groups.FirstOrDefault(g => g.Id == Filter.GroupId);
|
||||||
|
|
||||||
[Inject] DiscoveryService DiscoveryService { get; set; }
|
[Inject] DiscoveryService DiscoveryService { get; set; }
|
||||||
|
|||||||
+14
-2
@@ -14,8 +14,9 @@
|
|||||||
<h1 class="font-bold truncate">@Localizer["Explore"]</h1>
|
<h1 class="font-bold truncate">@Localizer["Explore"]</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-x-auto flex-none">
|
<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">
|
<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-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-flame"></i> @Localizer["Popular posts"]</Header></DTab>
|
||||||
<DTab><Header><i class="ion-md-link"></i> @Localizer["Shared links"]</Header></DTab>
|
<DTab><Header><i class="ion-md-link"></i> @Localizer["Shared links"]</Header></DTab>
|
||||||
@@ -139,6 +140,17 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
@code {
|
@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
|
enum ExploreTab
|
||||||
{
|
{
|
||||||
Hashtags,
|
Hashtags,
|
||||||
|
|||||||
@@ -1853,4 +1853,7 @@
|
|||||||
<data name="Every persona of this login is signed out on this device." xml:space="preserve">
|
<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>
|
<value>Tutte le persone di questo accesso escono da questo dispositivo.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Feeds" xml:space="preserve">
|
||||||
|
<value>Feed</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1853,4 +1853,7 @@
|
|||||||
<data name="Every persona of this login is signed out on this device." xml:space="preserve">
|
<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>
|
<value>Every persona of this login is signed out on this device.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Feeds" xml:space="preserve">
|
||||||
|
<value>Feeds</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
using decePubClient.Models;
|
using decePubClient.Models;
|
||||||
|
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
@@ -41,6 +42,9 @@ public class AppStatusService(IJSRuntime jsRuntime)
|
|||||||
|
|
||||||
public bool IsMobileMedia() => JsSyncRuntime.Invoke<bool>("isMobileMedia");
|
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 bool CanShare() => JsSyncRuntime.Invoke<bool>("canShareStuff");
|
||||||
|
|
||||||
public async ValueTask ShareTextUrl(string title, string text, Uri uri, CancellationToken cancellationToken = default) =>
|
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")
|
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)
|
// a phone: narrower than Tailwind's md, or held sideways (as short as the short: variant)
|
||||||
window.isMobileMedia = () => {
|
window.isMobileMedia = () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in new issue
Block a user