@tangible/ui 0.0.5 → 0.0.7

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.
@@ -8,18 +8,13 @@ import { isDev } from '../../utils/is-dev.js';
8
8
  // Switch Component
9
9
  // =============================================================================
10
10
  //
11
- // <button role="switch"> with animated thumb. Uses new tui-switch SCSS.
12
- // Existing tui-toggle CSS untouched (stays for CSS-only usage).
11
+ // <button role="switch"> with animated thumb. Uses tui-switch SCSS.
13
12
  //
14
13
  // Bare (no label): returns <button> directly for Field.Control cloneElement.
15
- // With label: wraps in <label>, native label click focuses the button.
16
- //
17
- // CSS token API:
18
- // --tui-switch-accent Accent color for on state
19
- // --tui-switch-track-off Track color when off
14
+ // With label: wraps in <label>, label-text clicks forward to button.
20
15
  //
21
16
  // =============================================================================
22
- // Props that should route to the <button>, not the wrapper
17
+ // Props that should route to the <button>, not the wrapper.
23
18
  const BUTTON_PROPS = new Set([
24
19
  'id',
25
20
  'aria-describedby',
@@ -29,6 +24,7 @@ const BUTTON_PROPS = new Set([
29
24
  'aria-labelledby',
30
25
  'form',
31
26
  'tabIndex',
27
+ 'onClick',
32
28
  'onFocus',
33
29
  'onBlur',
34
30
  ]);
@@ -57,13 +53,18 @@ export const Switch = forwardRef(function Switch({ checked: controlledChecked, d
57
53
  // Extract onClick from rest so prop spreading can't override internal handler
58
54
  const { onClick: onClickProp, ...restWithoutClick } = rest;
59
55
  const handleClick = (e) => {
56
+ if (disabled)
57
+ return;
60
58
  setChecked((prev) => !prev);
61
59
  onClickProp?.(e);
62
60
  };
63
61
  const isChecked = checked ?? false;
62
+ // Shared button element — single source of truth for both render paths.
63
+ // In labeled mode, buttonProps/sizeClass are set differently (see below).
64
+ const renderButton = (extraProps, extraClass) => (_jsx("button", { ref: composeRefs(internalRef, externalRef), type: "button", role: "switch", "aria-checked": isChecked, disabled: disabled, className: cx('tui-switch__track', extraClass), onClick: handleClick, ...extraProps, children: _jsx("span", { className: "tui-switch__thumb" }) }));
64
65
  // Bare: no label — Field.Control can inject id/aria-* directly
65
66
  if (!label) {
66
- return (_jsx("button", { ref: composeRefs(internalRef, externalRef), type: "button", role: "switch", "aria-checked": isChecked, disabled: disabled, className: cx('tui-switch__track', sizeClass, isChecked && 'is-checked', className), onClick: handleClick, ...restWithoutClick, children: _jsx("span", { className: "tui-switch__thumb" }) }));
67
+ return renderButton(restWithoutClick, cx(sizeClass, className));
67
68
  }
68
69
  // Split rest props: some go on button, some on wrapper
69
70
  const buttonProps = {};
@@ -76,16 +77,29 @@ export const Switch = forwardRef(function Switch({ checked: controlledChecked, d
76
77
  wrapperProps[key] = val;
77
78
  }
78
79
  }
79
- // With label: <label> wrapper provides click-anywhere-to-toggle natively.
80
- // We intercept the label click to toggle state and prevent the browser's
81
- // default label→button activation (which would double-toggle).
80
+ // DEV: warn if button-like props leaked to the wrapper
81
+ if (isDev()) {
82
+ const suspect = Object.keys(wrapperProps).filter((k) => k.startsWith('aria-') || k.startsWith('on') || k === 'tabIndex');
83
+ if (suspect.length > 0) {
84
+ console.warn(`Switch: Props [${suspect.join(', ')}] ended up on the <label> wrapper. ` +
85
+ 'Add them to BUTTON_PROPS if they belong on the <button>.');
86
+ }
87
+ }
88
+ // <label> wrapping a <button> causes native click-forwarding (button IS
89
+ // a labelable element). Without preventDefault, clicking the button fires
90
+ // handleClick AND the forwarded click — double-toggle.
91
+ // We suppress native forwarding. Clicks on the button are handled by its
92
+ // own onClick (handleClick). Clicks on the label text toggle state here.
82
93
  const handleLabelClick = (e) => {
83
- // Prevent native label click from also activating the button
84
94
  e.preventDefault();
95
+ // Clicks on/inside the button are handled by handleClick (via bubbling)
96
+ if (internalRef.current?.contains(e.target))
97
+ return;
85
98
  if (disabled)
86
99
  return;
87
100
  internalRef.current?.focus();
88
101
  setChecked((prev) => !prev);
89
102
  };
90
- return (_jsxs("label", { className: cx('tui-switch', sizeClass, disabled && 'is-disabled', className), onClick: handleLabelClick, ...wrapperProps, children: [_jsx("button", { ref: composeRefs(internalRef, externalRef), type: "button", role: "switch", "aria-checked": isChecked, disabled: disabled, className: cx('tui-switch__track', isChecked && 'is-checked'), ...buttonProps, children: _jsx("span", { className: "tui-switch__thumb" }) }), _jsx("span", { className: "tui-switch__label", children: label })] }));
103
+ return (_jsxs("label", { className: cx('tui-switch', sizeClass, disabled && 'is-disabled', className), onClick: handleLabelClick, ...wrapperProps, children: [renderButton(buttonProps), _jsx("span", { className: "tui-switch__label", children: label })] }));
91
104
  });
105
+ Switch.displayName = 'Switch';
@@ -104,7 +104,7 @@ function TabsRoot({ variant = 'underline', activationMode = 'auto', value: contr
104
104
  }, [registryVersion, activeValue, focusedValue, isControlled, onValueChange, getOrderedTabs]);
105
105
  // Dev-only: Tab-Panel pairing validation
106
106
  useEffect(() => {
107
- if (import.meta.env.DEV) {
107
+ if (isDev()) {
108
108
  // Defer validation to next microtask so all tabs/panels have registered
109
109
  queueMicrotask(() => {
110
110
  const tabValues = new Set(Array.from(tabsRef.current.keys()));
@@ -167,7 +167,7 @@ function TabsList({ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy,
167
167
  const { orientation, activationMode, activeValue, focusedValue, getOrderedTabs, onSelect, setFocusedValue, tabsRef, } = useTabsContext();
168
168
  // Dev-only: Warn if missing accessible name
169
169
  useEffect(() => {
170
- if (import.meta.env.DEV) {
170
+ if (isDev()) {
171
171
  if (!ariaLabel && !ariaLabelledBy) {
172
172
  console.warn('Tabs.List: Missing accessible name. Provide aria-label or aria-labelledby.');
173
173
  }
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React, { useCallback, useEffect, useRef, cloneElement, isValidElement, } from 'react';
3
3
  import { cx } from '../../utils/cx.js';
4
+ import { isDev } from '../../utils/is-dev.js';
4
5
  // =============================================================================
5
6
  // Toolbar Component
6
7
  // =============================================================================
@@ -66,7 +67,7 @@ function ToolbarRoot({ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledB
66
67
  const originalTabIndexMapRef = useRef(new WeakMap());
67
68
  // Dev warning for missing accessible name
68
69
  useEffect(() => {
69
- if (import.meta.env?.DEV && !ariaLabel && !ariaLabelledBy) {
70
+ if (isDev() && !ariaLabel && !ariaLabelledBy) {
70
71
  console.warn('[Toolbar] aria-label or aria-labelledby is required for accessibility.');
71
72
  }
72
73
  }, [ariaLabel, ariaLabelledBy]);
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useCallback, useEffect, useId, useMemo, useRef, useState, cloneElement, isValidElement, } from 'react';
3
+ import { isDev } from '../../utils/is-dev.js';
3
4
  import { useFloating, offset, flip, shift, arrow, autoUpdate, FloatingPortal, FloatingArrow, } from '@floating-ui/react';
4
5
  import { cx } from '../../utils/cx.js';
5
6
  import { getPortalRootFor } from '../../utils/portal.js';
@@ -165,7 +166,7 @@ function TooltipContentComponent({ side = 'top', align = 'center', sideOffset =
165
166
  // Dev warning: tooltips should not contain interactive content (WCAG 1.4.13)
166
167
  // Use Popover for interactive overlays instead
167
168
  useEffect(() => {
168
- if (import.meta.env?.DEV && open && refs.floating.current) {
169
+ if (isDev() && open && refs.floating.current) {
169
170
  const interactive = refs.floating.current.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])');
170
171
  if (interactive.length > 0) {
171
172
  console.warn('[Tooltip] Contains interactive elements which violates WCAG 1.4.13. ' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangible/ui",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Tangible Design System",
5
5
  "type": "module",
6
6
  "main": "./components/index.js",
@@ -79,20 +79,18 @@
79
79
  "types": "./components/Dropdown/Dropdown.d.ts"
80
80
  }
81
81
  },
82
- "dependencies": {
83
- "@floating-ui/react": "^0.27.16",
84
- "@tanstack/react-table": "^8.21.3"
85
- },
86
82
  "peerDependencies": {
83
+ "@floating-ui/react": ">=0.27.0",
84
+ "@tanstack/react-table": ">=8.0.0",
87
85
  "react": ">=18.0.0",
88
86
  "react-dom": ">=18.0.0"
89
87
  },
90
88
  "peerDependenciesMeta": {
91
- "react": {
92
- "optional": false
89
+ "@floating-ui/react": {
90
+ "optional": true
93
91
  },
94
- "react-dom": {
95
- "optional": false
92
+ "@tanstack/react-table": {
93
+ "optional": true
96
94
  }
97
95
  },
98
96
  "homepage": "https://github.com/tangibleinc/tangible-ui",