एलिमेंट
NumberInput
नंबर इनपुट एक ऐसी संख्या रखता है जिसे उपयोगकर्ता टाइप करता है या स्टेप से बदलता है: मात्रा, कीमत, आकार। उसका व्यवहार उसी के पास है: उसका फ़ील्ड एक संख्या लेता है, या उस संख्या की ओर बढ़ता टेक्स्ट (-, 1.), और बाकी सब अस्वीकार कर देता है, दूसरे कीबोर्ड के अंक और चिह्न भी; उसके बटन, Up और Down, और स्क्रीन रीडर के इंक्रीमेंट और डिक्रीमेंट संख्या को step से स्टेप करते हैं, उसके दशमलव बनाए रखते हुए, और min और max पर रुक जाते हैं; फ़ील्ड छोड़ने पर टाइप की गई संख्या इन सीमाओं में बाँध दी जाती है। रूप-रंग ऐप का है: NumberInput के अपने कोई रंग, आकार, बॉर्डर, आइकन या स्ट्रिंग नहीं होते। वह डिक्रीमेंट बटन, ऐप के prefix और suffix के बीच के फ़ील्ड, और इंक्रीमेंट बटन से बना है (या उनके बीच के फ़ील्ड और दोनों बटनों वाले एक कॉलम से, controlsRight), और ऐप हर भाग को स्टाइल करता है और बटनों में सामग्री भरता है।
import { type JSX, mount, signal, Text, View } from "nsh";
import { NumberInput, Switch } from "nsh/base";
import { desktop, focusRing, hoverFill, Log, Panel, size, Split, ui } from "./palette";
// The parts: geometry and states in classes, colors from the theme's
// tokens. The input is one recessed well (a stronger border under the
// pointer, the focus ring while its field edits: `focus-within:`); the
// buttons light up under the pointer and while pressed (a transition the
// reduced-motion setting stills) and fade where a step would do nothing (at
// `min` / `max`) or while the input is disabled, without their hover. A
// pointer gets the compact sizes. The price stacks its buttons on the right
// with its currency beside the field.
const frame = [
desktop ? "h-8 rounded-lg" : "h-11 rounded-xl",
"flex-row items-stretch overflow-hidden border border-input bg-field",
"enabled:hover:border-faint focus-within:outline-2 focus-within:outline-info disabled:opacity-50",
].join(" ");
const stepButton = [
desktop ? "w-8" : "w-11",
"items-center justify-center transition-colors duration-100 motion-reduce:transition-none",
"enabled:hover:bg-accent active:bg-track disabled:opacity-35",
].join(" ");
const stackedButton = "flex-1 items-center justify-center transition-colors duration-100 motion-reduce:transition-none enabled:hover:bg-accent active:bg-track disabled:opacity-35";
const field = () => ({
flex: 1,
minWidth: 0,
paddingHorizontal: desktop ? 8 : 10,
fontSize: desktop ? 14 : 16,
color: ui().text,
placeholderColor: ui().faint,
fontVariant: ["tabular-nums" as const],
});
/** A minus or a plus drawn with two bars in the text color. */
function Glyph(props: { plus?: boolean; small?: boolean }) {
const length = props.small ? 8 : desktop ? 10 : 12;
const bar = () => ({ position: "absolute" as const, borderRadius: 1, backgroundColor: ui().text });
return (
<View style={{ pointerEvents: "none", width: length, height: length }}>
<View style={{ ...bar(), left: 0, right: 0, top: length / 2 - 0.75, height: 1.5 }} />
{props.plus ? <View style={{ ...bar(), top: 0, bottom: 0, left: length / 2 - 0.75, width: 1.5 }} /> : null}
</View>
);
}
function Row(props: { label: string; detail: string; children: JSX.Element }) {
return (
<View class={`flex-row items-center gap-3 ${desktop ? "px-3 py-2.5" : "px-4 py-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>
<View style={{ width: desktop ? 148 : 168 }}>{props.children}</View>
</View>
);
}
const show = (v: number | null) => (v === null ? "null" : `${v}`);
function App() {
const tickets = signal<number | null>(2);
const price = signal<number | null>(19.95);
const temperature = signal<number | null>(null);
const disabled = signal(false);
const events = signal<string[]>([]);
const change = (name: string, value: { set: (v: number | null) => void }) => ({ value: next }: { value: number | null }) => {
value.set(next);
events.set([`${name} → ${show(next)}`, ...events()].slice(0, 5));
};
const line = () => <View style={{ height: 1, marginLeft: desktop ? 12 : 16, backgroundColor: ui().line }} />;
const sides = () => ({
disabled: disabled(),
class: frame,
inputStyle: { ...field(), textAlign: "center" as const },
decrementButtonClass: `${stepButton} border-r border-line`,
incrementButtonClass: `${stepButton} border-l border-line`,
decrementButton: <Glyph />,
incrementButton: <Glyph plus />,
});
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>number-input</Text>
<Text style={{ color: ui().muted }}>
{desktop ? "Type a number, click − and +, or press ↑ ↓ (Page Up / Down: ten steps) in the field" : "Type a number, or tap − and +"}
</Text>
<Split
gap={14}
inspector={
<>
<Panel title="value · disabled">
<Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>
tickets {show(tickets())} · price {show(price())} · °C {show(temperature())}
{disabled() ? " (disabled)" : ""}
</Text>
<Switch
checked={disabled()}
onChange={({ checked }) => disabled.set(checked)}
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={disabled() ? 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={disabled() ? { backgroundColor: ui().onAccent } : ui().raised}
>
<Text style={{ flex: 1, fontSize: size.label, color: ui().text }}>Disabled</Text>
</Switch>
</Panel>
<Log title="onChange({ value })" lines={events()} minHeight={92} />
</>
}
>
<View style={{ ...ui().card, borderRadius: 16, overflow: "hidden" }}>
<Row label="Tickets" detail="1 to 10">
<NumberInput
value={tickets()}
onChange={change("tickets", tickets)}
min={1}
max={10}
accessibilityLabel="Tickets"
{...sides()}
/>
</Row>
{line()}
<Row label="Price" detail="Steps of 0.05">
<NumberInput
value={price()}
onChange={change("price", price)}
min={0}
step={0.05}
accessibilityLabel="Price"
controlsRight
disabled={disabled()}
class={frame}
inputStyle={field()}
controlsClass={`${desktop ? "w-6" : "w-9"} border-l border-line`}
incrementButtonClass={`${stackedButton} border-b border-line`}
decrementButtonClass={stackedButton}
incrementButton={<Glyph plus small />}
decrementButton={<Glyph small />}
suffix={<Text style={{ alignSelf: "center", paddingRight: 8, fontSize: size.detail, color: ui().muted }}>USD</Text>}
/>
</Row>
{line()}
<Row label="Temperature" detail="−20 to 40 °C, halves">
<NumberInput
value={temperature()}
onChange={change("temperature", temperature)}
min={-20}
max={40}
step={0.5}
placeholder="Not set"
accessibilityLabel="Temperature"
{...sides()}
/>
</Row>
</View>
</Split>
</View>
);
}
mount(App);use nsh_base::{number_input, theme};
use nsh_core::prelude::*;
fn tickets(count: Signal<Option<f64>>) -> impl IntoElement {
number_input()
.value(move || count.get())
.min(1.0)
.max(10.0)
.accessibility_label("Tickets")
.on_change(move |next: &Option<f64>| count.set(*next))
.h(px(32.0))
.flex_row()
.rounded(px(8.0))
.border_1()
.border_color(move || theme().input)
.input_style(|s| s.flex_1().text_center().text_color(move || theme().foreground))
.decrement_button_style(|s| s.w(px(32.0)).items_center().justify_center())
.increment_button_style(|s| s.w(px(32.0)).items_center().justify_center())
.decrement_button(text("−"))
.increment_button(text("+"))
}import { mount, signal, Text } from "nsh";
import { NumberInput } from "nsh/base";
function App() {
const tickets = signal<number | null>(2);
return (
<NumberInput
value={tickets()}
onChange={({ value }) => tickets.set(value)}
min={1}
max={10}
accessibilityLabel="Tickets"
class="h-8 flex-row overflow-hidden rounded-lg border border-input bg-field focus-within:outline-2 focus-within:outline-ring"
inputClass="flex-1 text-center text-foreground"
decrementButtonClass="w-8 items-center justify-center enabled:hover:bg-accent active:bg-muted disabled:opacity-40"
incrementButtonClass="w-8 items-center justify-center enabled:hover:bg-accent active:bg-muted disabled:opacity-40"
decrementButton={<Text>−</Text>}
incrementButton={<Text>+</Text>}
/>
);
}
mount(App);NumberInput nsh/base से आता है, जो Rust में nsh-base क्रेट है। Rust में number_input() एक इनपुट बनाता है, जिसमें हर प्रॉप के लिए एक सेटर (.value(..), .input_style(..)), हर इवेंट के लिए एक लिस्नर मेथड (.on_change(..)), इनपुट के अपने लिए टाइप्ड स्टाइल मेथड, और फ़ील्ड के आसपास की चीज़ों के लिए .prefix(..) / .suffix(..) होते हैं। इसका मान एक Option<f64> है।
#मान
मान एक संख्या है, या खाली फ़ील्ड के लिए null। ऐप इसे value से अपने पास रखता है और वही मान सेट करता है जो onChange रिपोर्ट करता है, जैसा nsh/base के हर कंट्रोल में होता है; या वह मान को defaultValue से इनपुट पर छोड़ देता है, और फिर भी बदलाव सुनता है।
फ़ील्ड का टेक्स्ट और मान वहाँ अलग रखे जाते हैं जहाँ ज़रूरी है, ताकि उपयोगकर्ता खुलकर टाइप कर सके:
onChangeतब वह संख्या रिपोर्ट करता है जो टेक्स्ट पढ़ने पर बनती है, जब भी वह संख्या बदलती है: जब तक टेक्स्ट किसी संख्या के रूप में नहीं पढ़ा जाता (खाली,-,.) तब तकnull;1के बाद1.टाइप करने पर, या1.5के बाद1.50टाइप करने पर, कुछ रिपोर्ट नहीं होता।- जब तक टेक्स्ट उसी मान के रूप में पढ़ा जाता है जो इनपुट के पास है, तब तक टेक्स्ट वैसा ही रहता है जैसा उपयोगकर्ता ने टाइप किया, इसलिए जो कंट्रोल्ड इनपुट हर रिपोर्ट किया गया मान वापस दे देता है वह
1.और2.50को बनाए रखता है। ऐप द्वारा सेट किया गया ऐसा मान जो इनपुट ने रिपोर्ट नहीं किया, फ़ील्ड में सबसे छोटे ऐसे टेक्स्ट के रूप में दिखता है जो उसे पढ़ने पर वही मान दे (0.3,12)। - टाइप करते समय संख्या
minऔरmaxके बाहर हो सकती है (min={10}के साथ 15 की ओर जाता 1), और उसे वैसे ही रिपोर्ट किया जाता है जैसी टाइप की गई। फ़ील्ड छोड़ने पर, या Return / Enter पर, उसे सीमा में बाँध दिया जाता है और वही रिपोर्ट होता है; जो टेक्स्ट किसी संख्या के रूप में नहीं पढ़ा जाता वह जैसा है वैसा रहता है। - एक स्टेप (बटन, कुंजी, स्क्रीन रीडर की क्रिया) अगला मान
onChangeसे माँगता है; कंट्रोल्ड मान के साथ फ़ील्ड तब बदलता है जब ऐप उसे वापस देता है, स्टेप के दशमलव बनाए रखते हुए (max={100}तक99.5ऊपर करने पर100.0दिखता है), इसलिए जो ऐप किसी स्टेप को अस्वीकार करता है उसमें फ़ील्ड वहीं रहता है। ऐप द्वारा अस्वीकार किया गया संपादन फ़ील्ड में तब तक रहता है जब तक वह संपादित हो रहा है, और फ़ील्ड छोड़ने पर चला जाता है।
const price = signal<number | null>(19.95);
<NumberInput
value={price()}
onChange={({ value }) => price.set(value)}
onSubmit={({ value }) => save(value)}
min={0}
step={0.05}
placeholder="Price"
accessibilityLabel="Price"
/>;
फ़ील्ड ये लेता है:
- अंक, एक दशमलव बिंदु और सबसे पहले एक चिह्न (
+या-); अक्षर, दूसरा बिंदु या चिह्न, या कहीं और आया चिह्न अस्वीकार कर दिया जाता है, और फ़ील्ड अपने टेक्स्ट पर लौट जाता है। - फ़ुल-विड्थ अंक, चिह्न और बिंदु, माइनस चिह्न (U+2212) और आइडियोग्राफ़िक फ़ुल स्टॉप (
。), जिन्हें उनके ASCII रूप में पढ़ा जाता है, जैसा चीनी और जापानी इनपुट मेथड टाइप करते हैं। - दशमलव बिंदु के रूप में अल्पविराम, जब तक टेक्स्ट में कोई बिंदु नहीं है (
3,25यानी 3.25), जैसा कई भाषाओं के कीबोर्ड की दशमलव कुंजी टाइप करती है।
#प्रॉप्स
| प्रॉप | Rust | प्रकार | डिफ़ॉल्ट | विवरण |
|---|---|---|---|---|
value | .value(..) | number | null | कोई नहीं | मान (कंट्रोल्ड): एक संख्या, या खाली फ़ील्ड के लिए null। ऐप वही मान सेट करता है जो onChange रिपोर्ट करता है। |
defaultValue | .default_value(..) | number | null | कोई नहीं | शुरुआती मान, जब तक value सेट नहीं है; तब इनपुट अपना मान खुद रखता है। |
min | .min(..) | number | कोई नहीं | सबसे छोटा मान: स्टेप यहीं रुक जाते हैं, इससे नीचे टाइप किया गया मान फ़ील्ड छोड़ने पर इसी तक बाँध दिया जाता है। |
max | .max(..) | number | कोई नहीं | सबसे बड़ा मान, जैसा min ऊपर से करता है; min से नीचे का मान min होता है। |
step | .step(..) | number | 1 | एक स्टेप कितना जाता है (Page Up / Page Down: दस स्टेप); जो धनात्मक संख्या नहीं है वह 1 होता है। |
disabled | .disabled(..) | boolean | false | टाइपिंग, स्टेप या फ़ोकस नहीं; disabled: क्लासेस इनपुट और उसके भागों पर लागू होती हैं। |
readonly | .readonly(..) | boolean | false | टेक्स्ट को चुना और कॉपी किया जा सकता है पर बदला नहीं जा सकता, न टाइपिंग से न स्टेप से; बटन अक्षम होते हैं। |
placeholder | .placeholder(..) | string | कोई नहीं | फ़ील्ड खाली होने पर दिखता है। |
accessibilityLabel | .accessibility_label(..) | string | कोई नहीं | फ़ील्ड के लिए स्क्रीन रीडर क्या बोलते हैं, जब कोई दिखने वाला लेबल उसे नाम नहीं देता। |
controlsRight | .controls_right(..) | boolean | false | दोनों बटन फ़ील्ड के दाईं ओर एक कॉलम में, इंक्रीमेंट डिक्रीमेंट के ऊपर। इनपुट बनते समय पढ़ा जाता है। |
inputClass, inputStyle | .input_style(|s| ..) | string, StyleProp<InputStyle> | कोई नहीं | फ़ील्ड: उसका बॉक्स और उसका टेक्स्ट (color, fontSize, textAlign, placeholderColor, caretColor)। |
decrementButtonClass, decrementButtonStyle | .decrement_button_style(|s| ..) | string, StyleProp<ViewStyle> | कोई नहीं | डिक्रीमेंट बटन। |
incrementButtonClass, incrementButtonStyle | .increment_button_style(|s| ..) | string, StyleProp<ViewStyle> | कोई नहीं | इंक्रीमेंट बटन। |
decrementButton, incrementButton | .decrement_button(..), .increment_button(..) | JSX.Element | कोई नहीं | बटनों की सामग्री: एक चिह्न, एक आइकन। |
controlsClass, controlsStyle | .controls_style(|s| ..) | string, StyleProp<ViewStyle> | कोई नहीं | वह कॉलम जो controlsRight के साथ दोनों बटन रखता है। |
prefix | .prefix(..) | JSX.Element | कोई नहीं | फ़ील्ड से पहले क्या आता है, उसके बाईं ओर के बटन के बाद: मुद्रा चिह्न, आइकन। |
suffix | .suffix(..) | JSX.Element | कोई नहीं | फ़ील्ड के बाद क्या आता है, उसके दाईं ओर के बटनों से पहले: इकाई, मुद्रा। |
class, style | स्टाइल मेथड | string, StyleProp<ViewStyle> | कोई नहीं | इनपुट स्वयं: फ़ील्ड और बटनों के आसपास की पंक्ति। |
#इवेंट
| इवेंट | Rust | पेलोड | कब |
|---|---|---|---|
onChange | .on_change(|value: &Option<f64>| ..) | { value: number | null } | वह संख्या बदली जो फ़ील्ड पढ़ने पर देता है (टाइपिंग, स्टेप, स्क्रीन रीडर का इंक्रीमेंट या डिक्रीमेंट, सीमा से बाहर मान के साथ फ़ील्ड छोड़ना)। |
onSubmit | .on_submit(|value: &Option<f64>| ..) | { value: number | null } | फ़ील्ड में Return / Enter: मान, min और max में बाँधा हुआ। |
onFocus | .on_focus(..) | कोई नहीं | फ़ील्ड को कीबोर्ड फ़ोकस मिला। |
onBlur | .on_blur(..) | कोई नहीं | फ़ील्ड का कीबोर्ड फ़ोकस चला गया (उसका मान पहले min और max में बाँधा गया)। |
#भाग
| भाग | कहाँ | स्टाइल किसे से होता है |
|---|---|---|
| इनपुट | class, style | |
| डिक्रीमेंट बटन | इनपुट में, पहला (controlsRight के साथ: कंट्रोल कॉलम में, दूसरा) | decrementButtonClass, decrementButtonStyle; उसकी सामग्री decrementButton |
| फ़ील्ड | इनपुट में | inputClass, inputStyle |
| प्रीफ़िक्स | इनपुट में, फ़ील्ड से पहले | अपने तत्व (prefix) |
| सफ़िक्स | इनपुट में, फ़ील्ड के बाद | अपने तत्व (suffix) |
| इंक्रीमेंट बटन | इनपुट में, आख़िरी (controlsRight के साथ: कंट्रोल कॉलम में, पहला) | incrementButtonClass, incrementButtonStyle; उसकी सामग्री incrementButton |
| कंट्रोल कॉलम | इनपुट में, आख़िरी, controlsRight के साथ | controlsClass, controlsStyle |
इनपुट एक पंक्ति है जिसका लेआउट ऐप तय करता है: class पर flex-row, और फ़ील्ड पर flex-1 ताकि बटनों के बाद बची जगह उसे मिल जाए। भाग जो स्टेट दिखाते हैं:
- इनपुट: जब उसका फ़ील्ड संपादित हो रहा हो तब
focus-within:(पूरे इनपुट के चारों ओर एक रिंग),disabled:। - फ़ील्ड: संपादन के दौरान
focus:,disabled:। - बटन: पॉइंटर के नीचे
hover:, दबे होने परactive:, जब इनपुट अक्षम या रीड-ओनली हो और जहाँ कोई स्टेप कुछ नहीं करेगा वहाँdisabled:(minपर डिक्रीमेंट,maxपर इंक्रीमेंट)। हover कोenabled:(enabled:hover:bg-accent) के पीछे रखें ताकि अक्षम बटन हाइलाइट न हो।
स्टेट की गति का चुनाव ऐप का है (transition-colors), और motion-reduce: उसे उन लोगों के लिए स्थिर रखता है जो कम गति माँगते हैं।
#कीबोर्ड
जब फ़ील्ड संपादित हो रहा हो, इनपुट WAI-ARIA की स्पिन बटन कुंजियाँ लेता है, जो दबाए रखने पर दोहराई जाती हैं:
| कुंजी | क्या करती है |
|---|---|
| Up | एक स्टेप ऊपर। |
| Down | एक स्टेप नीचे। |
| Page Up, Page Down | दस स्टेप ऊपर या नीचे। |
Home और End फ़ील्ड के ही रहते हैं: वे टेक्स्ट में कैरेट को ले जाते हैं। बटन कीबोर्ड फ़ोकस के पड़ाव नहीं हैं: Tab फ़ील्ड से सीधे इनपुट के बाद की चीज़ पर जाता है, और बटन पर प्रेस फ़ोकस को फ़ील्ड में ही रहने देता है (डेस्कटॉप पर, अगर फ़ोकस कहीं और था तो प्रेस उसे फ़ील्ड पर ले आता है; टच स्क्रीन पर वह उसे वहीं छोड़ देता है, ताकि कीबोर्ड न खुले)।
#एक्सेसिबिलिटी
- फ़ील्ड एक स्पिन बटन (
"spinbutton") है, जिसके साथ उसका लेबल, उसका मान (वह संख्या जो वह पढ़ने पर देता है,minऔरmaxके बीच) और उसका टेक्स्ट होता है। - स्क्रीन रीडर की इंक्रीमेंट और डिक्रीमेंट क्रियाएँ (VoiceOver का ऊपर और नीचे स्वाइप) उसे वैसे ही स्टेप करती हैं जैसे Up और Down करते हैं।
- बटन स्क्रीन रीडर से छिपे होते हैं, जैसे ब्राउज़र के नंबर फ़ील्ड के होते हैं: फ़ील्ड के अपने इंक्रीमेंट और डिक्रीमेंट उसे स्टेप करते हैं।
- जब
disabledहो, स्क्रीन रीडर बताते हैं कि फ़ील्ड धुंधला (अक्षम) है।
सामान्य रूप से लेबल, रोल और क्रियाओं के लिए एक्सेसिबिलिटी देखें।
#प्लैटफ़ॉर्म नोट्स
- फ़ील्ड प्लैटफ़ॉर्म का टेक्स्ट फ़ील्ड है (Input): कीबोर्ड, कैरेट, चयन, कॉपी और पेस्ट, और इनपुट मेथड सिस्टम के हैं। फ़ोन दशमलव कीबोर्ड दिखाते हैं, और जब
min0 से कम हो या सेट न हो तब चिह्न के साथ। - हर प्लैटफ़ॉर्म वही बनाता है जो ऐप स्टाइल करता है। Up / Down और Page Up / Page Down हर प्लैटफ़ॉर्म पर हार्डवेयर कीबोर्ड से इनपुट तक पहुँचते हैं।