एलिमेंट
Progress
प्रोग्रेस बार दिखाता है कि ज्ञात विस्तार वाला कोई कार्य कितना आगे बढ़ा है (डाउनलोड, अपलोड, सेटअप का कोई चरण), या indeterminate होने पर यह कि अज्ञात विस्तार वाला कोई कार्य जारी है। स्क्रीन रीडर को जो मिलता है, यानी progress bar role, 0 से 100 की रेंज और वैल्यू, और इंडिकेटर की चौड़ाई (ट्रैक की चौड़ाई का value प्रतिशत), यह सब प्रोग्रेस बार संभालता है। रूप ऐप का होता है: Progress के अपने कोई रंग, आकार, रेडियस, मोशन या लेबल नहीं होते। यह एक ट्रैक से बनता है जिसके अंदर इंडिकेटर होता है, फिर इसके चिल्ड्रन (एक लेबल, टेक्स्ट के रूप में वैल्यू), और हर भाग को ऐप स्टाइल करता है।
import { cleanup, mount, signal, Text, View } from "nsh";
import { reduceMotion } from "nsh";
import { Button, Progress, Switch } from "nsh/base";
import { button, buttonText, desktop, focusRing, hoverFill, Log, Panel, size, Split, ui } from "./palette";
// The parts: geometry and motion in classes, colors from the palette. The
// label row comes first (`flex-col-reverse`: the children follow the track),
// its value tabular and at the trailing edge. An indeterminate indicator is a
// third of the track that sweeps from end to end (the app flips its
// translation, a transition moves it), and rests in the middle with reduced
// motion. A determinate one is the whole track, slid back by what is left to
// do, so a new value glides there (the track clips it). The pointer's hover
// and the keyboard's focus ring come from the palette. Buttons are as wide as
// their labels with a pointer.
const pad = desktop ? "px-3 py-3" : "px-4 py-4";
const track = "h-1.5 overflow-hidden rounded-full";
const SWEEP = 800;
const indicator = (indeterminate: boolean, end: boolean) =>
!indeterminate
? "h-full w-full rounded-full transition-transform duration-180 ease-[cubic-bezier(0.2,0,0,1)] motion-reduce:transition-none"
: reduceMotion()
? "h-full w-1/3 rounded-full translate-x-full"
: `h-full w-1/3 rounded-full transition-transform duration-700 ease-in-out ${end ? "translate-x-[200%]" : "translate-x-0"}`;
function Transfer(props: { label: string; detail: string; value: number; indeterminate?: boolean; end: boolean }) {
return (
<Progress
value={props.value}
indeterminate={props.indeterminate}
class={`flex-col-reverse gap-2 ${pad}`}
trackClass={track}
trackStyle={ui().inset}
indicatorClass={indicator(!!props.indeterminate, props.end)}
indicatorStyle={{ backgroundColor: ui().accent, translate: props.indeterminate ? undefined : `${props.value - 100}% 0` }}
>
<View class="flex-row items-baseline gap-3">
<View style={{ flex: 1, gap: 2 }}>
<Text style={{ fontSize: size.label, fontWeight: "500", color: ui().text }}>{props.label}</Text>
<Text style={{ fontSize: size.detail, color: ui().muted }}>{props.detail}</Text>
</View>
<Text style={{ fontSize: size.label, color: ui().muted, fontVariant: ["tabular-nums"], textAlign: "right" }}>
{props.indeterminate ? "—" : `${Math.round(props.value)}%`}
</Text>
</View>
</Progress>
);
}
function Action(props: { label: string; disabled?: boolean; onPress: () => void }) {
return (
<Button
onPress={props.onPress}
disabled={props.disabled}
{...button("secondary", { style: desktop ? {} : { flex: 1 } })}
>
<Text style={buttonText()}>{props.label}</Text>
</Button>
);
}
function App() {
const sent = signal(35);
const running = signal(false);
const unknown = signal(false);
const events = signal<string[]>([]);
const log = (line: string) => events.set([line, ...events()].slice(0, 5));
// The sweep of the indeterminate bars: still with reduced motion.
const end = signal(false);
const sweep = setInterval(() => {
if (!reduceMotion()) end.set(!end());
}, SWEEP);
cleanup(() => clearInterval(sweep));
let timer: ReturnType<typeof setInterval> | undefined;
const stop = () => {
if (timer !== undefined) clearInterval(timer);
timer = undefined;
running.set(false);
};
const start = () => {
if (sent() >= 100) sent.set(0);
running.set(true);
log(`Start at ${sent()}%`);
timer = setInterval(() => {
sent.set(Math.min(100, sent() + 5));
if (sent() >= 100) {
stop();
log("Done: value 100");
}
}, 200);
};
const line = () => <View style={{ height: 1, marginLeft: desktop ? 12 : 16, backgroundColor: ui().line }} />;
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>progress</Text>
<Text style={{ color: ui().muted }}>{desktop ? "Click Start, or Tab to it and press Space" : "Tap Start to send the photos"}</Text>
<Split
gap={14}
inspector={
<>
<Panel title="value · indeterminate">
<Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>
{unknown() ? "indeterminate (no value for screen readers)" : `value ${sent()} of 100`}
</Text>
<View class={desktop ? "flex-row self-start gap-2" : "flex-row gap-2"}>
<Action label={running() ? "Pause" : sent() >= 100 ? "Again" : "Start"} disabled={unknown()} onPress={() => (running() ? (stop(), log(`Pause at ${sent()}%`)) : start())} />
<Action
label="Reset"
disabled={sent() === 0}
onPress={() => {
stop();
sent.set(0);
log("Reset: value 0");
}}
/>
</View>
<Switch
checked={unknown()}
onChange={({ checked }) => {
unknown.set(checked);
if (checked) stop();
log(checked ? "Size unknown: indeterminate" : "Size known: value " + sent());
}}
class={`-mx-2 flex-row-reverse items-center justify-between gap-3 rounded-lg px-2 ${desktop ? "py-1" : "py-2"} active:opacity-70`}
hoverStyle={hoverFill()}
focusStyle={focusRing()}
trackClass={`${desktop ? "w-9" : "w-12"} rounded-full p-0.5 transition-colors motion-reduce:transition-none`}
trackStyle={unknown() ? ui().accentFill : ui().inset}
thumbClass={desktop ? "size-4 rounded-full transition-transform motion-reduce:transition-none checked:translate-x-4" : "size-5.5 rounded-full transition-transform motion-reduce:transition-none checked:translate-x-5"}
thumbStyle={unknown() ? { backgroundColor: ui().onAccent } : ui().raised}
>
<Text style={{ flex: 1, fontSize: size.label, color: ui().text }}>Size unknown</Text>
</Switch>
</Panel>
<Log title="events" lines={events()} minHeight={92} />
</>
}
>
<View style={{ ...ui().card, borderRadius: 16, overflow: "hidden" }}>
<Transfer
label="Sending 24 photos"
detail={unknown() ? "Counting what to send" : sent() >= 100 ? "Sent" : `${Math.round((sent() / 100) * 24)} of 24`}
value={sent()}
indeterminate={unknown()}
end={end()}
/>
{line()}
<Transfer label="Preparing the backup" detail="Its size is not known yet" value={0} indeterminate end={end()} />
{line()}
<Transfer label="Library" detail="212 GB of 256 GB" value={83} end={end()} />
</View>
</Split>
</View>
);
}
mount(App);use nsh_base::{progress, theme};
use nsh_core::prelude::*;
fn upload(sent: Signal<f32>) -> impl IntoElement {
progress()
.value(sent)
.accessibility_label("Uploading photos")
.gap_2()
.track_style(|s| s.h(px(8.0)).rounded_full().overflow_hidden().bg(move || theme().muted))
.indicator_style(|s| s.h_full().rounded_full().bg(move || theme().primary))
.child(
text(move || format!("{:.0}%", sent.get()))
.text_sm()
.text_color(move || theme().muted_foreground),
)
}import { mount, signal, Text } from "nsh";
import { Progress } from "nsh/base";
function App() {
const sent = signal(40);
return (
<Progress
value={sent()}
accessibilityLabel="Uploading photos"
class="gap-2"
trackClass="h-2 overflow-hidden rounded-full bg-muted"
indicatorClass="h-full rounded-full bg-primary"
>
<Text class="text-sm text-muted-foreground">{`${sent()}%`}</Text>
</Progress>
);
}
mount(App);Progress nsh/base से आता है, जो Rust में nsh-base crate है। वैल्यू ऐप की होती है: प्रोग्रेस बार वही value दिखाता है जो ऐप उसे देता है, जिसे 0 से 100 के बीच सीमित किया जाता है। Rust में progress() हर prop के लिए एक सेटर (.value(..), .indicator_style(..)), प्रोग्रेस बार के लिए टाइप्ड स्टाइल मेथड और उसके लेबल के लिए .child(..) के साथ एक प्रोग्रेस बार बनाता है।
#Props
| Prop | Rust | टाइप | डिफ़ॉल्ट | विवरण |
|---|---|---|---|---|
value | .value(..) | number | 0 | कार्य कितना आगे बढ़ा है, 0 से 100 तक के प्रतिशत के रूप में (रेंज से बाहर होने पर निकटतम सिरा)। इंडिकेटर ट्रैक की चौड़ाई का उतना ही हिस्सा होता है, और स्क्रीन रीडर इसे बोलते हैं। |
indeterminate | .indeterminate(..) | boolean | false | कार्य का विस्तार अज्ञात है: स्क्रीन रीडर बिना वैल्यू वाला प्रोग्रेस बार सुनते हैं, और इंडिकेटर की चौड़ाई ऐप तय करता है (indicatorClass)। |
accessibilityLabel | .accessibility_label(..) | string | कोई नहीं | स्क्रीन रीडर क्या बोलते हैं: क्या प्रगति पर है, जब इसके चिल्ड्रन में ऐसा कोई टेक्स्ट नहीं जो यह बताए। |
trackClass, trackStyle | .track_style(|s| ..) | string, StyleProp<ViewStyle> | कोई नहीं | ट्रैक, वह भाग जिसे इंडिकेटर भरता है। |
indicatorClass, indicatorStyle | .indicator_style(|s| ..) | string, StyleProp<ViewStyle> | कोई नहीं | इंडिकेटर, ट्रैक के अंदर: उसकी ऊँचाई, रंग और मोशन। |
children | .child(..) | JSX.Element | कोई नहीं | ट्रैक के बाद की सामग्री: एक लेबल या टेक्स्ट के रूप में वैल्यू, जिसे स्क्रीन रीडर बोलते हैं। |
class, style | स्टाइल मेथड | string, StyleProp<ViewStyle> | कोई नहीं | प्रोग्रेस बार स्वयं: ट्रैक और लेबल के चारों ओर का कॉलम (या रो)। |
#भाग
| भाग | अंदर | स्टाइल करने का तरीका |
|---|---|---|
| प्रोग्रेस बार | class, style | |
| ट्रैक | प्रोग्रेस बार, सबसे पहले | trackClass, trackStyle |
| इंडिकेटर | ट्रैक | indicatorClass, indicatorStyle; इसकी चौड़ाई ट्रैक की चौड़ाई का value प्रतिशत है |
| लेबल | प्रोग्रेस बार, ट्रैक के बाद | इसके अपने एलिमेंट (children) |
इंडिकेटर ट्रैक के अग्रणी किनारे से शुरू होता है; overflow-hidden और ट्रैक पर रेडियस इसके सिरे को ट्रैक के साथ गोल करते हैं। w-* क्लास या indicatorStyle में width वैल्यू की चौड़ाई की जगह ले लेता है, और इसी तरह एक indeterminate बार को अपनी चौड़ाई मिलती है।
#Indeterminate
जब तक विस्तार अज्ञात है, indeterminate सेट करें और इंडिकेटर को ऐप की अपनी चौड़ाई और मोशन दें। मोशन ऐप का चुनाव है, और motion-reduce: उन लोगों के लिए उसे स्थिर रखता है जो कम मोशन माँगते हैं:
<Progress
value={0}
indeterminate={connecting()}
trackClass="h-2 overflow-hidden rounded-full bg-muted"
indicatorClass={
connecting()
? "h-full w-1/3 rounded-full bg-primary animate-pulse motion-reduce:animate-none"
: "h-full rounded-full bg-primary"
}
/>
#सुलभता
- स्क्रीन रीडर के लिए प्रोग्रेस बार एक ही एलिमेंट है, जिसका role
"progressbar"है, रेंज 0 से 100 है और उसकी वैल्यू है (VoiceOver और TalkBack इसे प्रतिशत के रूप में बोलते हैं); ट्रैक और इंडिकेटर उसके भाग हैं। इसका लेबल इसके चिल्ड्रन का टेक्स्ट है; जब चिल्ड्रन में टेक्स्ट न हो तोaccessibilityLabelदें। indeterminateहोने पर यह role और रेंज बनाए रखता है और इसकी कोई वैल्यू नहीं होती (ब्राउज़र में:aria-valuenowछोड़ दिया जाता है, जैसा कि ARIA indeterminate प्रोग्रेस बार के लिए कहता है)।- यह न प्रेस लेता है, न कीबोर्ड फ़ोकस: यह केवल जानकारी देता है, और कार्य को शुरू या बंद करने वाले कंट्रोल ऐप के होते हैं।
लेबल, roles और announcements के बारे में सामान्य जानकारी के लिए एक्सेसिबिलिटी देखें।
#प्लेटफ़ॉर्म नोट्स
- हर प्लेटफ़ॉर्म वही बनाता है जो ऐप स्टाइल करता है; इंडिकेटर की चौड़ाई तुरंत
valueका अनुसरण करती है, और जब ऐप बदलाव को एनिमेट करता है तो बदलाव एनिमेट होता है। - स्क्रीन रीडर: VoiceOver (iOS, macOS) वैल्यू को प्रतिशत के रूप में और बार को बार-बार अपडेट होने वाले के रूप में सुनता है; TalkBack, UI Automation (Windows), GTK की accessibility (Linux) और ब्राउज़र (
aria-valuenow) को रेंज और वैल्यू मिलती है।