Setting chips
A setting that holds a list of names or numbers, shown as removable chips with a field to add one.
On this page
Code
import { SettingChips } from "@bosunkit/components/setting-chips";
import type { SaveState } from "@bosunkit/components/setting-control";
import {
useEffect,
useRef,
useState,
type Dispatch,
type ReactElement,
type RefObject,
type SetStateAction,
} from "react";
export const description: string =
"Three list settings in one card: crew names whose saves land, Telegram chat ids whose saves fail and roll " +
"back, and the passage's harbors shown read-only.";
type Entries = (string | number)[];
interface ListSetting {
readonly values: Entries;
readonly state: SaveState;
}
/** How long the example's save takes to come back. */
const SAVE_MS: number = 800;
/**
* A list setting whose save comes back after SAVE_MS: it lands, or it fails and the list returns to the entries that
* are saved on the vessel.
*/
function useListSetting(initial: Entries, fails: boolean): [ListSetting, (next: Entries) => void] {
const [setting, setSetting]: [ListSetting, Dispatch<SetStateAction<ListSetting>>] = useState<ListSetting>({
values: initial,
state: "idle",
});
const saved: RefObject<Entries> = useRef<Entries>(initial);
const timer: RefObject<ReturnType<typeof setTimeout> | undefined> = useRef<ReturnType<typeof setTimeout> | undefined>(
undefined,
);
useEffect((): (() => void) => {
return (): void => {
clearTimeout(timer.current);
};
}, []);
function save(next: Entries): void {
setSetting({ values: next, state: "saving" });
clearTimeout(timer.current);
// Stands in for the PUT to the vessel's settings.
timer.current = setTimeout((): void => {
if (fails) {
setSetting({ values: saved.current, state: "error" });
return;
}
saved.current = next;
setSetting({ values: next, state: "saved" });
}, SAVE_MS);
}
return [setting, save];
}
function ignore(): void {
// A read-only list saves nothing.
}
export default function Showcase(): ReactElement {
const [crew, saveCrew]: [ListSetting, (next: Entries) => void] = useListSetting(
["Maya Lindqvist", "Tomás Herrera", "Ingrid Aalto"],
false,
);
const [chats, saveChats]: [ListSetting, (next: Entries) => void] = useListSetting([412_889_301, 77_120_455], true);
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "var(--space-md)",
width: "100%",
padding: "var(--space-md)",
border: "1px solid var(--border)",
borderRadius: "var(--radius-md)",
}}
>
<SettingChips
label="Anchor watch rota"
description="Who Bosun wakes, in this order, when an anchor alarm is not acknowledged within a minute."
values={crew.values}
state={crew.state}
onSave={saveCrew}
placeholder="Add a crew member…"
/>
<SettingChips
label="Telegram chats"
description="Chat ids that may ask Bosun about the vessel. In this example every save fails."
values={chats.values}
state={chats.state}
onSave={saveChats}
numeric
placeholder="Chat id"
/>
<SettingChips
label="Passage harbors"
description="Set by the passage plan, Palma to A Coruña; change them on the passage page."
values={["Palma de Mallorca", "Cartagena", "Gibraltar", "Lagos", "Cascais", "Baiona", "A Coruña"]}
state="idle"
onSave={ignore}
readOnly
/>
</div>
);
}
Setting chips edit a flat list of strings or numbers. Each entry is a chip with a remove button; Enter or leaving the
add field commits a new entry, and an entry already in the list is not added twice. Every change calls onSave with
the whole next list, and the save chip beside the control shows where that save is.
Examples
Named values
Code
import { SettingChips } from "@bosunkit/components/setting-chips";
import type { ReactElement } from "react";
export const description: string = "A list of names, each a chip with a remove button, and a field to add another.";
function ignore(): void {
// The example saves nothing.
}
export default function NamedValues(): ReactElement {
return <SettingChips label="Disabled" values={["podcast", "music"]} state="idle" onSave={ignore} />;
}
Numeric
Code
import { SettingChips } from "@bosunkit/components/setting-chips";
import type { ReactElement } from "react";
export const description: string =
"A list of numbers: a new entry is read as a number, and text that is not one is not added.";
function ignore(): void {
// The example saves nothing.
}
export default function Numeric(): ReactElement {
return <SettingChips label="Allowed Ids" values={[1234]} state="idle" onSave={ignore} numeric />;
}
Motion
By default an added entry scales in and a removed one scales out, while the others keep their place; the entries
there on the first render do not move. motion takes the kit's motion prop, and
motion={false} changes the list at once. A removed entry takes no press while it leaves, so a second tap cannot
remove its neighbor. Under reduced motion the entries only fade.
Code
import { SettingChips } from "@bosunkit/components/setting-chips";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"Type a port and press Enter, or remove one: by default an added port scales in and a removed one scales out while " +
"the others keep their place; the list below, with motion={false}, changes at once.";
type Entries = (string | number)[];
export default function Motion(): ReactElement {
const [ports, setPorts]: [Entries, Dispatch<SetStateAction<Entries>>] = useState<Entries>([
"Falmouth",
"Brest",
"A Coruña",
]);
const [still, setStill]: [Entries, Dispatch<SetStateAction<Entries>>] = useState<Entries>(["Falmouth", "Brest"]);
return (
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-md)", width: "100%" }}>
<SettingChips label="Ports of refuge" values={ports} state="idle" onSave={setPorts} />
<SettingChips label="Ports of refuge, without motion" values={still} state="idle" onSave={setStill} motion={false} />
</div>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| labelrequired | string | – | The setting's name; it also names the add field. |
| description | string | undefined | – | What the setting does, under the name. |
| valuesrequired | readonly (string | number)[] | – | Current entries — strings or numbers, preserved on the wire. |
| staterequired | SaveState | – | Where the save of the last change is. |
| onSaverequired | (next: (string | number)[]) => void | – | Fired with the full next list on add or remove. |
| numeric | boolean | undefined | false | Coerce new entries to numbers (for example telegram allowed_ids). |
| readOnly | boolean | undefined | – | Whether the entries show without the add field and the remove buttons. |
| placeholder | string | undefined | – | The hint shown in the empty add field; "Add…" when not set. |
| motion | MotionProp | undefined | – | How the entries move. By default an added entry scales in and a removed one scales out, while the others keep
their place; the entries there on the first render do not move. A preset (fade, rise, scale) or
{ enter, exit } chooses; false turns it off. A removed entry takes no press while it leaves. |
Related
- Setting control: the switch, field and select that share the save chip.