Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ $acd-private-properties:
// token is substituted where it is declared (on :root) and inherited as that value, so a -full duration set on
// the accordion itself or on any ancestor short of a theme scope would never reach it. The shared factor still
// collapses it under reduced motion, as it does a duration set inline.
--bit-acd-dur-full: var(--bit-mot-duration-long-full, #{$mot-duration-long});
--bit-acd-dur-full: #{$mot-duration-long-full};
--bit-acd-dur: #{mot-scaled-duration(var(--bit-acd-dur-full))};

// Every rule that reaches from the root into a part of the accordion is chained down through the header line
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@
@media (prefers-reduced-motion: reduce) {
.bit-mdl-bna:not(.bit-fam):not(.bit-fam *),
.bit-mdl-bnb:not(.bit-fam):not(.bit-fam *) {
animation-duration: var(--bit-mot-duration-long-full, #{$mot-duration-long});
animation-duration: $mot-duration-long-full;
animation-timing-function: steps(1, end);
}

Expand Down
94 changes: 64 additions & 30 deletions src/BlazorUI/Bit.BlazorUI/Styles/Fluent/motion.fluent.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// The -full tokens hold the unreduced values and are never touched by the reduced-motion query
// below, so they stay the single place to theme a duration: overriding --bit-mot-duration-full
// changes both the normal value and the one .bit-fam restores. Components always read the plain
// --bit-mot-* tokens; only the reduced-motion opt-out (.bit-fam in general.scss) reads -full.
// changes both the normal value and the one .bit-fam restores. That is why BitTheme.Motion writes
// them, never the plain tokens: a plain token set inline would outrank the reduced-motion query.
// Components read the plain --bit-mot-* tokens; besides the aliases below, the only readers of -full
// are the $mot-*-full aliases of theme-variables.scss, for what stays meaningful under the
// preference (a color flash, not a movement).
//
// The literals live here, on :root alone; the plain tokens that alias them live in the twinned
// block below. A preset for another design system (the packaged Fluent 2 / Material / Cupertino
Expand Down Expand Up @@ -39,48 +42,79 @@
--bit-mot-loop-factor-full: 1;
}

// The plain tokens every component reads. They are aliases, so - exactly as in semantic-tokens.scss
// and family-tokens.scss - they need a scoped-preset twin: a custom property's var() references are
// The plain tokens every component reads. Each is the reduced value while the reduction is in force,
// and the unreduced -full value otherwise.
//
// The reduction is itself inherited state: the -reduced tokens hold the values it substitutes, and
// are declared only while it is in force - by the reduced-motion query on :root below, or again by a
// consumer re-applying it to a subtree - and reset to `initial` (no value, so the alias falls back to
// -full) by .bit-fam, the ForceAnimation opt-out, for its element and everything under it. So nothing
// ever has to decide again whether motion is reduced where the aliases are re-substituted: every
// place that declares them reads that answer off its ancestors.
//
// They are aliases, so - exactly as in semantic-tokens.scss and family-tokens.scss - they are
// re-substituted wherever one of their inputs can change: a custom property's var() references are
// substituted at the element that DEFINES it, and descendants inherit the already-substituted value.
// Declared on :root alone, --bit-mot-duration would freeze the DOCUMENT's 200ms inside an element
// carrying bit-theme="material", whose own --bit-mot-duration-full says 250ms.
//
// The twin is wrapped in :where() so it contributes no specificity of its own: the reduced-motion
// query below re-declares the same two selectors and wins on source order, and .bit-fam (0,1,0) -
// the ForceAnimation opt-out in general.scss - still outranks both, including on an element that
// carries bit-theme and bit-fam at once.
:root,
:where(:root [bit-theme]) {
--bit-mot-duration: var(--bit-mot-duration-full);
--bit-mot-duration-short: var(--bit-mot-duration-short-full);
--bit-mot-duration-long: var(--bit-mot-duration-long-full);
--bit-mot-duration-factor: var(--bit-mot-duration-factor-full);
// carrying bit-theme="material", whose own --bit-mot-duration-full says 250ms, or inside an inline
// theme overlay - a BitThemeProvider wrapper, or ApplyBitThemeAsync on an element - that re-values a
// -full token on its own element (the motion group of theme-overlay.scss). Every theme builds the
// aliases the same way, so this one rule serves every scoped region and every overlay; and it would
// freeze the reduced 0.01ms inside a .bit-fam subtree, which is why .bit-fam re-substitutes them too.
@mixin motion-aliases {
--bit-mot-duration: var(--bit-mot-duration-reduced, var(--bit-mot-duration-full));
--bit-mot-duration-short: var(--bit-mot-duration-short-reduced, var(--bit-mot-duration-short-full));
--bit-mot-duration-long: var(--bit-mot-duration-long-reduced, var(--bit-mot-duration-long-full));
--bit-mot-duration-factor: var(--bit-mot-duration-factor-reduced, var(--bit-mot-duration-factor-full));

--bit-mot-duration-spinner: var(--bit-mot-duration-spinner-reduced, var(--bit-mot-duration-spinner-full));
--bit-mot-easing-spinner: var(--bit-mot-easing-spinner-reduced, var(--bit-mot-easing-spinner-full));
--bit-mot-loop-factor: var(--bit-mot-loop-factor-reduced, var(--bit-mot-loop-factor-full));
}

--bit-mot-duration-spinner: var(--bit-mot-duration-spinner-full);
--bit-mot-easing-spinner: var(--bit-mot-easing-spinner-full);
--bit-mot-loop-factor: var(--bit-mot-loop-factor-full);
// The twins are wrapped in :where() so they contribute no specificity of their own: what a
// consumer's own rule declares for a plain token on a scoped region or an overlay element still
// wins there, as it does on any other element.
:root,
:where(:root [bit-theme]),
:where(:root [bit-theme-overlay~="motion"]) {
@include motion-aliases;
}

// 0.01ms instead of 0ms: a zero-duration transition is never run, so it never fires its
// transitionend event, which would hang anything awaiting it (a zero-duration animation still
// fires animationend; the one value keeps both kinds of wait working alike).
// Twinned like the block above, and for the same reason in reverse: without the twin the collapse
// would never reach a scoped preset region, whose durations are re-substituted on its own element.
@media (prefers-reduced-motion: reduce) {
:root,
:where(:root [bit-theme]) {
--bit-mot-duration: 0.01ms;
--bit-mot-duration-short: 0.01ms;
--bit-mot-duration-long: 0.01ms;
:root {
--bit-mot-duration-reduced: 0.01ms;
--bit-mot-duration-short-reduced: 0.01ms;
--bit-mot-duration-long-reduced: 0.01ms;
// A page-set duration collapses to the same 0.01ms (mot-scaled-duration adds it back in
// for exactly this case), and a page-set delay to nothing.
--bit-mot-duration-factor: 0;
--bit-mot-duration-factor-reduced: 0;

// Calmed rather than collapsed - see above. The spinner also drops its eased timing function,
// whose speeding up and slowing down is the part that draws the eye. Stopping a loader
// outright is never an option: it is the only signal that the work is still in progress.
--bit-mot-duration-spinner: 4s;
--bit-mot-easing-spinner: linear;
--bit-mot-loop-factor: 3;
--bit-mot-duration-spinner-reduced: 4s;
--bit-mot-easing-spinner-reduced: linear;
--bit-mot-loop-factor-reduced: 3;
}
}

// Opts a subtree out of the reduced-motion collapse (BitComponentBase.ForceAnimation): the reduction
// is withdrawn for this element and everything under it, and the aliases are substituted again here,
// where they now resolve to the -full values - without touching the document root. A scoped region
// or an overlay inside the subtree inherits the withdrawn reduction and so stays unreduced as well.
// It comes after the query above, so it still wins on source order on the root element itself.
.bit-fam {
--bit-mot-duration-reduced: initial;
--bit-mot-duration-short-reduced: initial;
--bit-mot-duration-long-reduced: initial;
--bit-mot-duration-factor-reduced: initial;
--bit-mot-duration-spinner-reduced: initial;
--bit-mot-easing-spinner-reduced: initial;
--bit-mot-loop-factor-reduced: initial;

@include motion-aliases;
}
20 changes: 0 additions & 20 deletions src/BlazorUI/Bit.BlazorUI/Styles/general.scss
Original file line number Diff line number Diff line change
Expand Up @@ -37,26 +37,6 @@ button {
}
}

// Opts a subtree out of the reduced-motion collapse in motion.fluent.scss (BitComponentBase.ForceAnimation).
// The durations are inherited custom properties, so re-pointing them at the -full tokens here restores the
// unreduced values for this element and everything under it, without touching the document root. The
// literal fallbacks keep this working for a consumer theme that defines --bit-mot-duration* by hand and
// therefore has no -full tokens to fall back on.
.bit-fam {
--bit-mot-duration: var(--bit-mot-duration-full, 200ms);
--bit-mot-duration-short: var(--bit-mot-duration-short-full, 100ms);
--bit-mot-duration-long: var(--bit-mot-duration-long-full, 300ms);
// Which is what restores a duration the page set on a component as well: every one of them is
// scaled by this factor (mot-scaled-duration), so none needs a restore of its own.
--bit-mot-duration-factor: var(--bit-mot-duration-factor-full, 1);

// The looping tokens slow down rather than collapse under reduced motion, but they are restored
// here the same way, so a spinner inside a ForceAnimation subtree turns at full speed again.
--bit-mot-duration-spinner: var(--bit-mot-duration-spinner-full, 1.3s);
--bit-mot-easing-spinner: var(--bit-mot-easing-spinner-full, cubic-bezier(0.53, 0.21, 0.29, 0.67));
--bit-mot-loop-factor: var(--bit-mot-loop-factor-full, 1);
}

// The chain of `display: contents` copies a relocated callout is moved into (Callouts.moveCalloutToBody) carries
// the classes of the ancestors it stands for, so that what they declare still inherits into the callout. A
// display: contents element still generates its ::before and ::after, which would draw the decorations of
Expand Down
4 changes: 4 additions & 0 deletions src/BlazorUI/Bit.BlazorUI/Styles/theme-overlay.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
// foreground --bit-clr-fg-pri every role foreground
// pri ... err --bit-clr-<role> that role's foreground and tint
// typography --bit-tpg-fs-*, --bit-tpg-fw-* the dialog title's size and weight, the field caption's weight
// motion --bit-mot-*-full the plain motion tokens, which alias the -full ones
// (motion.fluent.scss, not this mixin: every theme builds
// them alike, so one zero-specificity rule serves all,
// and they read whether motion is reduced off ancestors)
//
// This mixin emits the selectors a theme block re-declares a group on, mirroring the selectors of the
// block itself:
Expand Down
13 changes: 13 additions & 0 deletions src/BlazorUI/Bit.BlazorUI/Styles/theme-variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,14 @@ $box-shadow-focus-ring: var(--bit-shd-focus-ring);
$mot-duration: var(--bit-mot-duration);
$mot-duration-short: var(--bit-mot-duration-short);
$mot-duration-long: var(--bit-mot-duration-long);
//The unreduced durations: never collapsed by prefers-reduced-motion. Only for motion that stays
//meaningful under the preference - a change of color, not a movement (BitModal's refusal ring) - or
//for the default of a duration the page can set, which mot-scaled-duration collapses through the factor
//below instead (BitAccordion's transition). Each falls back to its plain token, for a consumer theme
//that declares only the plain ones by hand.
$mot-duration-full: var(--bit-mot-duration-full, var(--bit-mot-duration));
$mot-duration-short-full: var(--bit-mot-duration-short-full, var(--bit-mot-duration-short));
$mot-duration-long-full: var(--bit-mot-duration-long-full, var(--bit-mot-duration-long));
//A duration the PAGE sets on a component (a public --bit-<Component>-*-duration variable, a duration
//parameter written inline) is none of the tokens above, so it is scaled by this factor instead - 1
//normally, 0 under prefers-reduced-motion, 1 again wherever the tokens are restored (.bit-fam, an app's
Expand Down Expand Up @@ -595,6 +603,11 @@ $mot-easing-spinner: var(--bit-mot-easing-spinner);
//the loop factor multiplies literal durations inside calc(), so it carries a fallback of 1 to keep
//those animations valid for consumer-defined themes that never set the token.
$mot-loop-factor: var(--bit-mot-loop-factor, 1);
//The unreduced looping tokens, which the reduced-motion query never calms - for the same narrow uses
//as the unreduced durations above, with the same fallback to the plain token.
$mot-duration-spinner-full: var(--bit-mot-duration-spinner-full, var(--bit-mot-duration-spinner));
$mot-easing-spinner-full: var(--bit-mot-easing-spinner-full, var(--bit-mot-easing-spinner));
$mot-loop-factor-full: var(--bit-mot-loop-factor-full, var(--bit-mot-loop-factor, 1));

/*---- Typography ----*/
$tg-font-family: var(--bit-tpg-font-family);
Expand Down
14 changes: 14 additions & 0 deletions src/BlazorUI/Bit.BlazorUI/Utils/BitCss.var.cs
Original file line number Diff line number Diff line change
Expand Up @@ -613,18 +613,32 @@ public static class Card
}
}

/// <summary>
/// The motion tokens. The durations and their factor, the spinner pair and the loop factor come in two forms: the plain
/// one is what a style reads, and what <c>prefers-reduced-motion</c> collapses (or calms) and
/// <c>ForceAnimation</c> restores; the <c>...Full</c> one holds the unreduced value the plain one aliases,
/// and is the one to set - setting the plain one, on an element above all, would outrank the
/// reduced-motion query. <see cref="BitThemeMotion"/> writes the <c>...Full</c> ones.
/// </summary>
public static class Motion
{
public const string Duration = "--bit-mot-duration";
public const string DurationShort = "--bit-mot-duration-short";
public const string DurationLong = "--bit-mot-duration-long";
public const string DurationFactor = "--bit-mot-duration-factor";
public const string DurationFull = "--bit-mot-duration-full";
public const string DurationShortFull = "--bit-mot-duration-short-full";
public const string DurationLongFull = "--bit-mot-duration-long-full";
public const string DurationFactorFull = "--bit-mot-duration-factor-full";
public const string Easing = "--bit-mot-easing";
public const string EasingDecelerate = "--bit-mot-easing-decelerate";
public const string EasingAccelerate = "--bit-mot-easing-accelerate";
public const string DurationSpinner = "--bit-mot-duration-spinner";
public const string EasingSpinner = "--bit-mot-easing-spinner";
public const string LoopFactor = "--bit-mot-loop-factor";
public const string DurationSpinnerFull = "--bit-mot-duration-spinner-full";
public const string EasingSpinnerFull = "--bit-mot-easing-spinner-full";
public const string LoopFactorFull = "--bit-mot-loop-factor-full";
}

public static class Layout
Expand Down
2 changes: 2 additions & 0 deletions src/BlazorUI/Bit.BlazorUI/Utils/Theme/BitTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ namespace BitBlazorUI {
['err', ['--bit-clr-err']],
['typography', ['--bit-tpg-fs-2xs', '--bit-tpg-fs-xs', '--bit-tpg-fs-sm', '--bit-tpg-fs-md', '--bit-tpg-fs-lg', '--bit-tpg-fs-xl', '--bit-tpg-fs-2xl', '--bit-tpg-fs-3xl', '--bit-tpg-fs-4xl',
'--bit-tpg-fw-light', '--bit-tpg-fw-regular', '--bit-tpg-fw-medium', '--bit-tpg-fw-semibold', '--bit-tpg-fw-bold']],
['motion', ['--bit-mot-duration-full', '--bit-mot-duration-short-full', '--bit-mot-duration-long-full', '--bit-mot-duration-factor-full',
'--bit-mot-duration-spinner-full', '--bit-mot-easing-spinner-full', '--bit-mot-loop-factor-full']],
];
// The page's own background - what a status bar sits above in nearly every app.
const THEME_COLOR_VARIABLE = '--bit-clr-bg-pri';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
namespace Bit.BlazorUI;

/// <summary>
/// The motion tokens of a theme. The durations and their factor, the spinner pair and the loop factor are written to their unreduced
/// <c>-full</c> tokens (<c>--bit-mot-duration-full</c>, ...), as a preset declares them, so a value set here is still
/// collapsed (or calmed) under <c>prefers-reduced-motion</c> and restored by <c>ForceAnimation</c>.
/// </summary>
public class BitThemeMotion
{
/// <summary>The standard duration of a transition or a one-shot animation (<c>--bit-mot-duration-full</c>).</summary>
public string? Duration { get; set; }

/// <summary>The short duration, for small state changes (<c>--bit-mot-duration-short-full</c>).</summary>
public string? DurationShort { get; set; }

/// <summary>The long duration, for large surfaces entering and leaving (<c>--bit-mot-duration-long-full</c>).</summary>
public string? DurationLong { get; set; }

/// <summary>
/// Multiplies every duration a page sets on a component itself - a public
/// <c>--bit-&lt;Component&gt;-*-duration</c> variable or a duration parameter - which none of the durations
/// above reach. It is a unitless number: 1 keeps the pace the page asked for, and reduced motion drops it
/// to 0 so those durations collapse along with the theme's own (<c>--bit-mot-duration-factor</c>).
/// <br />
/// Like the durations above, it is written to the effective token, and a theme applied inline outranks the
/// reduced-motion query on <c>:root</c>: a value set here keeps every duration a page sets running for a reader
/// who asked for reduced motion. Leave it unset unless the app answers that preference itself, and scale the
/// page's durations with <c>--bit-mot-duration-factor-full</c> in CSS instead, which feeds both states.
/// to 0 so those durations collapse along with the theme's own (<c>--bit-mot-duration-factor-full</c>).
/// </summary>
public string? DurationFactor { get; set; }

Expand Down
Loading
Loading