@lotics/ui 1.10.0 → 1.11.1

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.
Files changed (55) hide show
  1. package/package.json +24 -7
  2. package/src/alert.tsx +35 -5
  3. package/src/avatar.tsx +28 -3
  4. package/src/back_button.tsx +4 -2
  5. package/src/button.tsx +35 -5
  6. package/src/calendar/calendar_view.tsx +127 -0
  7. package/src/calendar/dates.ts +102 -0
  8. package/src/calendar/index.ts +20 -0
  9. package/src/calendar/layout.test.ts +103 -0
  10. package/src/calendar/layout.ts +142 -0
  11. package/src/calendar/month_view.tsx +159 -0
  12. package/src/calendar/time_grid_view.tsx +263 -0
  13. package/src/calendar/types.ts +67 -0
  14. package/src/checkbox_input.tsx +9 -3
  15. package/src/command_menu.tsx +50 -4
  16. package/src/dialog.tsx +1 -1
  17. package/src/download.ts +14 -2
  18. package/src/form_field.tsx +77 -25
  19. package/src/form_switch.tsx +22 -3
  20. package/src/gantt/gantt_view.tsx +145 -0
  21. package/src/gantt/index.ts +5 -0
  22. package/src/gantt/scale.test.ts +47 -0
  23. package/src/gantt/scale.ts +92 -0
  24. package/src/gantt/types.ts +51 -0
  25. package/src/grid/select_header_cell.tsx +1 -0
  26. package/src/icon.tsx +14 -8
  27. package/src/icon_button.tsx +10 -4
  28. package/src/index.css +11 -0
  29. package/src/kanban/constants.ts +18 -0
  30. package/src/kanban/default_renderers.tsx +160 -0
  31. package/src/kanban/drag_preview.tsx +157 -0
  32. package/src/kanban/index.ts +13 -0
  33. package/src/kanban/insert_card_zone.tsx +135 -0
  34. package/src/kanban/kanban_board.tsx +616 -0
  35. package/src/kanban/kanban_card.tsx +312 -0
  36. package/src/kanban/kanban_column.tsx +487 -0
  37. package/src/kanban/placeholders.tsx +54 -0
  38. package/src/kanban/types.ts +116 -0
  39. package/src/landmark.tsx +34 -0
  40. package/src/menu_button.tsx +21 -0
  41. package/src/menu_list_item.tsx +3 -0
  42. package/src/number_input.tsx +10 -1
  43. package/src/pill_button.tsx +1 -0
  44. package/src/popover.tsx +47 -2
  45. package/src/popover_header.tsx +4 -2
  46. package/src/pressable_highlight.tsx +24 -0
  47. package/src/radio_picker.tsx +63 -5
  48. package/src/section_heading.tsx +5 -3
  49. package/src/skip_link.tsx +46 -0
  50. package/src/switch.tsx +9 -1
  51. package/src/switch_button.tsx +3 -0
  52. package/src/tabs.tsx +81 -19
  53. package/src/text.tsx +33 -0
  54. package/src/text_input_field.tsx +31 -0
  55. package/src/tooltip.tsx +43 -6
package/src/text.tsx CHANGED
@@ -22,8 +22,25 @@ export interface TextProps {
22
22
  decoration?: TextDecorationLine;
23
23
  style?: RNTextProps["style"];
24
24
  onPress?: () => void;
25
+ /** ID used to target this element from `accessibilityLabelledBy` / `aria-describedby`. */
26
+ nativeID?: string;
27
+ accessibilityRole?: RNTextProps["accessibilityRole"];
28
+ accessibilityLabel?: string;
29
+ accessibilityElementsHidden?: boolean;
30
+ importantForAccessibility?: RNTextProps["importantForAccessibility"];
31
+ /**
32
+ * Renders as a heading of this rank (1–6): exposes `accessibilityRole="header"`
33
+ * and `aria-level`, giving the page a real heading outline. Appearance still
34
+ * comes from `size`/`weight` — `level` is the semantic rank only. Use one
35
+ * level-1 per page.
36
+ */
37
+ level?: HeadingLevel;
38
+ "aria-live"?: "off" | "polite" | "assertive";
39
+ "aria-hidden"?: boolean;
25
40
  }
26
41
 
42
+ export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
43
+
27
44
  type TextSize = "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
28
45
  type TextAlign = "left" | "right" | "center";
29
46
  type TextDecorationLine = "underline" | "lineThrough" | "underline lineThrough";
@@ -48,6 +65,14 @@ export function Text(props: TextProps) {
48
65
  transform,
49
66
  onPress,
50
67
  style,
68
+ nativeID,
69
+ accessibilityRole,
70
+ accessibilityLabel,
71
+ accessibilityElementsHidden,
72
+ importantForAccessibility,
73
+ level,
74
+ "aria-live": ariaLive,
75
+ "aria-hidden": ariaHidden,
51
76
  } = props;
52
77
 
53
78
  // On web, responsive font sizes are applied via CSS using data-text-size attribute.
@@ -57,6 +82,14 @@ export function Text(props: TextProps) {
57
82
  return (
58
83
  <RNText
59
84
  testID={testID}
85
+ nativeID={nativeID}
86
+ accessibilityRole={level != null ? "header" : accessibilityRole}
87
+ accessibilityLabel={accessibilityLabel}
88
+ accessibilityElementsHidden={accessibilityElementsHidden}
89
+ importantForAccessibility={importantForAccessibility}
90
+ aria-level={level}
91
+ aria-live={ariaLive}
92
+ aria-hidden={ariaHidden}
60
93
  {...webProps}
61
94
  style={[
62
95
  { color: getTextColor(color) },
@@ -12,6 +12,7 @@ import { ShortcutBadge } from "./shortcut_badge";
12
12
  import { fontFamilyRegular, getInputLineHeight, getInputTextStyle } from "./text_utils";
13
13
  import { useScreenSize } from "./use_screen_size";
14
14
  import { useAutoGrowHeight } from "./use_auto_grow_height";
15
+ import { useFormField } from "./form_field";
15
16
  import type { ShortcutDescriptor } from "./keyboard";
16
17
 
17
18
  interface TextInputFieldProps extends RNTextInputProps {
@@ -25,6 +26,13 @@ interface TextInputFieldProps extends RNTextInputProps {
25
26
  autoGrow?: boolean;
26
27
  /** Keyboard shortcut badge shown in the right slot when the input is empty. */
27
28
  shortcut?: string | ShortcutDescriptor;
29
+ /** Accessible name for the clear button. Default: "Clear". Pass a translated string from the consumer. */
30
+ clearLabel?: string;
31
+ // DOM-only ARIA attrs not declared on React Native's TextInputProps. They
32
+ // are forwarded verbatim to the underlying web input.
33
+ "aria-controls"?: string;
34
+ "aria-activedescendant"?: string;
35
+ "aria-autocomplete"?: "none" | "inline" | "list" | "both";
28
36
  }
29
37
 
30
38
  export function TextInputField(props: TextInputFieldProps) {
@@ -40,12 +48,29 @@ export function TextInputField(props: TextInputFieldProps) {
40
48
  disabled,
41
49
  autoGrow,
42
50
  shortcut,
51
+ clearLabel = "Clear",
43
52
  ref,
53
+ "aria-controls": ariaControls,
54
+ "aria-activedescendant": ariaActivedescendant,
55
+ "aria-autocomplete": ariaAutocomplete,
44
56
  ...inputProps
45
57
  } = props;
46
58
 
59
+ // Forwarded to the DOM input on RN Web. RN's TextInput type does not declare
60
+ // these attrs, but react-native-web passes unknown props through to the
61
+ // element, which is the correct behavior for combobox patterns.
62
+ const webAriaAttrs: Record<string, unknown> = {};
63
+ if (ariaControls !== undefined) webAriaAttrs["aria-controls"] = ariaControls;
64
+ if (ariaActivedescendant !== undefined) webAriaAttrs["aria-activedescendant"] = ariaActivedescendant;
65
+ if (ariaAutocomplete !== undefined) webAriaAttrs["aria-autocomplete"] = ariaAutocomplete;
66
+
47
67
  const { small } = useScreenSize();
48
68
  const lineHeight = getInputLineHeight(small);
69
+ const binding = useFormField();
70
+
71
+ // Describedby chains description and error so both are read. We join them
72
+ // explicitly here because React Native Web does not flatten array attrs.
73
+ const describedBy = [binding?.descriptionId, binding?.errorId].filter(Boolean).join(" ") || undefined;
49
74
 
50
75
  const minHeight =
51
76
  numberOfLines && numberOfLines > 1
@@ -90,11 +115,16 @@ export function TextInputField(props: TextInputFieldProps) {
90
115
  )}
91
116
  <RNTextInput
92
117
  {...inputProps}
118
+ {...webAriaAttrs}
93
119
  ref={mergedRef}
94
120
  value={value}
95
121
  onChangeText={handleChangeText}
96
122
  onFocus={inputProps.onFocus}
97
123
  onBlur={inputProps.onBlur}
124
+ nativeID={binding?.inputId ?? inputProps.nativeID}
125
+ accessibilityLabelledBy={binding?.labelId ?? inputProps.accessibilityLabelledBy}
126
+ aria-describedby={describedBy}
127
+ aria-invalid={binding?.invalid || undefined}
98
128
  style={[
99
129
  styles.input,
100
130
  getInputTextStyle(),
@@ -115,6 +145,7 @@ export function TextInputField(props: TextInputFieldProps) {
115
145
  {!!clearable && !!value ? (
116
146
  <IconButton
117
147
  icon="x"
148
+ tooltip={clearLabel}
118
149
  onPress={() => {
119
150
  onChangeText?.("");
120
151
  onClear?.();
package/src/tooltip.tsx CHANGED
@@ -190,17 +190,27 @@ export function useTooltip(options?: string | UseTooltipOptions) {
190
190
  const side = typeof options === "string" ? "top" : (options?.side ?? "top");
191
191
  const offset = typeof options === "string" ? undefined : options?.offset;
192
192
 
193
- const onMouseEnter = useCallback(
194
- (e: React.MouseEvent | { currentTarget: HTMLElement }) => {
193
+ const showFor = useCallback(
194
+ (target: unknown) => {
195
195
  if (!text || !context) return;
196
- const target = e.currentTarget as HTMLElement;
197
- if (!target) return;
196
+ // Tooltips are a web-only UI. `typeof` guards the native platforms where
197
+ // `HTMLElement` does not exist as a global, then `instanceof` narrows
198
+ // without an unchecked cast.
199
+ if (typeof HTMLElement === "undefined") return;
200
+ if (!(target instanceof HTMLElement)) return;
198
201
  const rect = target.getBoundingClientRect();
199
202
  context.show(text, rect, side, offset);
200
203
  },
201
204
  [text, context, side, offset],
202
205
  );
203
206
 
207
+ const onMouseEnter = useCallback(
208
+ (e: { currentTarget: unknown }) => {
209
+ showFor(e.currentTarget);
210
+ },
211
+ [showFor],
212
+ );
213
+
204
214
  const onMouseLeave = useCallback(() => {
205
215
  if (!context) return;
206
216
  context.hide();
@@ -211,15 +221,42 @@ export function useTooltip(options?: string | UseTooltipOptions) {
211
221
  context.hide();
212
222
  }, [context]);
213
223
 
214
- // If no text or no provider, return empty object
224
+ // Keyboard focus must reveal the same label as hover. `:focus-visible` CSS
225
+ // handles the visual ring; we tie tooltip visibility to real focus so
226
+ // screen-reader-off keyboard users still see names.
227
+ //
228
+ // Typed as `unknown` because these handlers spread onto both
229
+ // `react-native-web`'s `Pressable` (expects `NativeSyntheticEvent<TargetedEvent>`)
230
+ // and regular DOM elements (expect `React.FocusEvent`). At runtime on web,
231
+ // both shapes expose `currentTarget` as an `HTMLElement`, which is all we read.
232
+ const onFocus = useCallback(
233
+ (e: { currentTarget: unknown }) => {
234
+ showFor(e.currentTarget);
235
+ },
236
+ [showFor],
237
+ );
238
+
239
+ const onBlur = useCallback(() => {
240
+ if (!context) return;
241
+ context.hide();
242
+ }, [context]);
243
+
215
244
  if (!text || !context) {
216
- return {};
245
+ return {
246
+ onMouseEnter: undefined,
247
+ onMouseLeave: undefined,
248
+ onMouseDown: undefined,
249
+ onFocus: undefined,
250
+ onBlur: undefined,
251
+ };
217
252
  }
218
253
 
219
254
  return {
220
255
  onMouseEnter,
221
256
  onMouseLeave,
222
257
  onMouseDown,
258
+ onFocus,
259
+ onBlur,
223
260
  };
224
261
  }
225
262