sheer-ui 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,129 @@
1
+ import type { ReadableBox, ReadableBoxedValues, WritableBox } from './tools/index.js';
2
+ import { boolToStr, boolToEmptyStrOrUndef, boolToTrueOrUndef, getAriaChecked } from './attrs.js';
3
+ import { kbd } from './kbd.js';
4
+ import type { Orientation } from './index.js';
5
+ import type { BitsKeyboardEvent, BitsMouseEvent, WithRefOpts } from './types.js';
6
+ import type { RovingFocusGroup } from './roving-focus-group.js';
7
+ import { RovingFocusItem } from './roving-focus-item.svelte.js';
8
+
9
+ export type SelectionType = 'single' | 'multiple';
10
+
11
+ export const emptySelection = (type: SelectionType): string | string[] => (type === 'single' ? '' : []);
12
+
13
+ /**
14
+ * Membership over a bindable that is a string in single mode and a string array in multiple
15
+ * mode. The mode is fixed at construction, so a write always keeps the shape the consumer's
16
+ * prop was declared with.
17
+ */
18
+ export class SelectionValue {
19
+ readonly isMulti: boolean;
20
+ readonly #value: WritableBox<string | string[]>;
21
+
22
+ constructor(type: SelectionType, value: WritableBox<string | string[]>) {
23
+ this.isMulti = type === 'multiple';
24
+ this.#value = value;
25
+ }
26
+
27
+ includes(item: string): boolean {
28
+ const value = this.#value.current;
29
+ return this.isMulti ? (value as string[]).includes(item) : value === item;
30
+ }
31
+
32
+ /** Flips membership and returns whether the item is selected afterwards. */
33
+ toggle(item: string): boolean {
34
+ const selected = !this.includes(item);
35
+ if (this.isMulti) {
36
+ const value = this.#value.current as string[];
37
+ this.#value.current = selected ? [...value, item] : value.filter((v) => v !== item);
38
+ } else {
39
+ this.#value.current = selected ? item : '';
40
+ }
41
+ return selected;
42
+ }
43
+ }
44
+
45
+ /** What a selection item needs from the group that owns it. */
46
+ export interface SelectionGroup {
47
+ readonly selection: SelectionValue;
48
+ readonly disabled: ReadableBox<boolean>;
49
+ readonly orientation: ReadableBox<Orientation>;
50
+ readonly rovingFocusGroup: RovingFocusGroup;
51
+ /** Whether arrow keys move between the items; absent means always. */
52
+ readonly rovingFocus?: ReadableBox<boolean>;
53
+ /** A selection takes the roving tab stop in a toggle group; a toolbar's stays where it is. */
54
+ readonly selectionTakesTabStop: boolean;
55
+ /** Beyond the roving group's candidate attribute, which every item carries. */
56
+ readonly extraItemAttrs?: Record<string, ''>;
57
+ }
58
+
59
+ export interface SelectionItemOpts
60
+ extends
61
+ WithRefOpts,
62
+ ReadableBoxedValues<{
63
+ value: string;
64
+ disabled: boolean;
65
+ }> {}
66
+
67
+ export class SelectionItemState {
68
+ readonly opts: SelectionItemOpts;
69
+ readonly group: SelectionGroup;
70
+ readonly rovingItem: RovingFocusItem;
71
+ readonly #isDisabled = $derived.by(() => this.opts.disabled.current || this.group.disabled.current);
72
+ readonly isPressed = $derived.by(() => this.group.selection.includes(this.opts.value.current));
73
+
74
+ constructor(opts: SelectionItemOpts, group: SelectionGroup) {
75
+ this.opts = opts;
76
+ this.group = group;
77
+ this.rovingItem = new RovingFocusItem({
78
+ group: group.rovingFocusGroup,
79
+ ref: opts.ref,
80
+ enabled: group.rovingFocus,
81
+ });
82
+
83
+ this.onclick = this.onclick.bind(this);
84
+ this.onkeydown = this.onkeydown.bind(this);
85
+ }
86
+
87
+ #activate() {
88
+ if (this.#isDisabled) return;
89
+ const selected = this.group.selection.toggle(this.opts.value.current);
90
+ if (selected && this.group.selectionTakesTabStop) this.group.rovingFocusGroup.setCurrentTabStopId(this.opts.id.current);
91
+ }
92
+
93
+ onclick(_: BitsMouseEvent) {
94
+ this.#activate();
95
+ }
96
+
97
+ onkeydown(e: BitsKeyboardEvent) {
98
+ if (this.#isDisabled) return;
99
+ if (e.key === kbd.ENTER || e.key === kbd.SPACE) {
100
+ e.preventDefault();
101
+ this.#activate();
102
+ return;
103
+ }
104
+ this.rovingItem.handleKeydown(e);
105
+ }
106
+
107
+ readonly snippetProps = $derived.by(() => ({ pressed: this.isPressed }));
108
+
109
+ readonly props = $derived.by(
110
+ () =>
111
+ ({
112
+ id: this.opts.id.current,
113
+ role: this.group.selection.isMulti ? undefined : 'radio',
114
+ 'data-orientation': this.group.orientation.current,
115
+ 'data-disabled': boolToEmptyStrOrUndef(this.#isDisabled),
116
+ 'data-state': this.isPressed ? 'on' : 'off',
117
+ 'data-value': this.opts.value.current,
118
+ 'aria-pressed': this.group.selection.isMulti ? boolToStr(this.isPressed) : undefined,
119
+ 'aria-checked': this.group.selection.isMulti ? undefined : getAriaChecked(this.isPressed, false),
120
+ disabled: boolToTrueOrUndef(this.#isDisabled),
121
+ ...this.group.rovingFocusGroup.candidateAttrs,
122
+ ...this.group.extraItemAttrs,
123
+ //
124
+ onclick: this.onclick,
125
+ onkeydown: this.onkeydown,
126
+ ...this.rovingItem.props,
127
+ }) as const,
128
+ );
129
+ }
@@ -231,9 +231,9 @@ export class Column<TData, TValue = unknown> {
231
231
  get isVisible(): boolean {
232
232
  return this.#table.columnVisibility[this.id] ?? true;
233
233
  }
234
- toggleVisibility(visible?: boolean) {
235
- if (!this.canHide) return;
236
- this.#table.columnVisibility = { ...this.#table.columnVisibility, [this.id]: visible ?? !this.isVisible };
234
+ set isVisible(visible: boolean) {
235
+ if (!this.canHide || visible === this.isVisible) return;
236
+ this.#table.columnVisibility = { ...this.#table.columnVisibility, [this.id]: visible };
237
237
  }
238
238
 
239
239
  get filterValue(): unknown {