@byline/ui 6.4.0 → 6.6.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.
@@ -1,2 +1,18 @@
1
1
  import type { InputProps } from './input.js';
2
- export declare function InputPassword({ ...props }: InputProps): import("react").JSX.Element;
2
+ /**
3
+ * `type` and `endAdornment` are the two props this component exists to
4
+ * control, so they are removed from the public surface rather than left
5
+ * to be overridden.
6
+ *
7
+ * They used to be overridable by accident: props were spread AFTER them,
8
+ * so a caller passing the obvious `type="password"` silently defeated the
9
+ * toggle — the icon swapped, the field stayed masked. Omitting them turns
10
+ * that into a compile error instead of a bug someone has to notice.
11
+ */
12
+ export interface InputPasswordProps extends Omit<InputProps, 'type' | 'endAdornment'> {
13
+ /** Accessible name for the reveal control while the password is hidden. */
14
+ showPasswordLabel?: string;
15
+ /** Accessible name for the reveal control while the password is shown. */
16
+ hidePasswordLabel?: string;
17
+ }
18
+ export declare function InputPassword({ showPasswordLabel, hidePasswordLabel, ...props }: InputPasswordProps): import("react").JSX.Element;
@@ -1,22 +1,30 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
+ import clsx from "clsx";
3
4
  import { EyeClosedIcon } from "../../icons/eye-closed-icon.js";
4
5
  import { EyeOpenIcon } from "../../icons/eye-open-icon.js";
5
6
  import { Input } from "./input.js";
6
- function InputPassword({ ...props }) {
7
+ import input_password_module from "./input-password.module.js";
8
+ function InputPassword({ showPasswordLabel = 'Show password', hidePasswordLabel = 'Hide password', ...props }) {
7
9
  const [visible, setVisible] = useState(false);
8
10
  return /*#__PURE__*/ jsx(Input, {
11
+ ...props,
9
12
  type: visible ? 'text' : 'password',
10
- endAdornment: visible ? /*#__PURE__*/ jsx(EyeOpenIcon, {
11
- width: "18px",
12
- height: "18px",
13
- onClick: ()=>setVisible(!visible)
14
- }) : /*#__PURE__*/ jsx(EyeClosedIcon, {
15
- width: "18px",
16
- height: "18px",
17
- onClick: ()=>setVisible(!visible)
18
- }),
19
- ...props
13
+ endAdornment: /*#__PURE__*/ jsx("button", {
14
+ type: "button",
15
+ className: clsx('byline-input-password-toggle', input_password_module.toggle),
16
+ "aria-label": visible ? hidePasswordLabel : showPasswordLabel,
17
+ "aria-pressed": visible,
18
+ onMouseDown: (event)=>event.preventDefault(),
19
+ onClick: ()=>setVisible((current)=>!current),
20
+ children: visible ? /*#__PURE__*/ jsx(EyeOpenIcon, {
21
+ width: "18px",
22
+ height: "18px"
23
+ }) : /*#__PURE__*/ jsx(EyeClosedIcon, {
24
+ width: "18px",
25
+ height: "18px"
26
+ })
27
+ })
20
28
  });
21
29
  }
22
30
  export { InputPassword };
@@ -0,0 +1,5 @@
1
+ import "./input-password_module.css";
2
+ const input_password_module = {
3
+ toggle: "toggle-bNQhst"
4
+ };
5
+ export default input_password_module;
@@ -0,0 +1,21 @@
1
+ @layer byline-components {
2
+ :is(.toggle-bNQhst, .byline-input-password-toggle) {
3
+ color: inherit;
4
+ cursor: pointer;
5
+ background: none;
6
+ border: none;
7
+ justify-content: center;
8
+ align-items: center;
9
+ min-width: 1.375rem;
10
+ min-height: 1.375rem;
11
+ padding: 0;
12
+ display: flex;
13
+ }
14
+
15
+ :is(.toggle-bNQhst:focus-visible, .byline-input-password-toggle:focus-visible) {
16
+ outline: 1px solid oklch(from var(--field-ring, var(--blue-500)) l c h / .6);
17
+ outline-offset: 1px;
18
+ border-radius: var(--border-radius-sm, 3px);
19
+ }
20
+ }
21
+
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "private": false,
4
4
  "type": "module",
5
5
  "license": "MPL-2.0",
6
- "version": "6.4.0",
6
+ "version": "6.6.0",
7
7
  "engines": {
8
8
  "node": ">=20.9.0"
9
9
  },
@@ -55,7 +55,7 @@
55
55
  "@dnd-kit/sortable": "^10.0.0",
56
56
  "clsx": "^2.1.1",
57
57
  "date-fns": "^4.4.0",
58
- "motion": "^13.3.0",
58
+ "motion": "^13.4.0",
59
59
  "react-day-picker": "^10.0.1",
60
60
  "zod": "^4.6.5"
61
61
  },
@@ -64,10 +64,10 @@
64
64
  "react-dom": "^19.0.0"
65
65
  },
66
66
  "devDependencies": {
67
- "@biomejs/biome": "2.5.13",
67
+ "@biomejs/biome": "2.5.14",
68
68
  "@rsbuild/plugin-react": "^2.1.0",
69
69
  "@rslib/core": "^1.0.1",
70
- "@types/node": "^26.6.1",
70
+ "@types/node": "^26.6.2",
71
71
  "@types/react": "19.3.0",
72
72
  "@types/react-dom": "19.3.0",
73
73
  "@vitejs/plugin-react": "^6.1.1",
@@ -0,0 +1,43 @@
1
+ /**
2
+ * InputPassword — the reveal toggle inside the input's end adornment.
3
+ *
4
+ * Override handle:
5
+ * .byline-input-password-toggle — the reveal/hide button
6
+ *
7
+ * It is a `<button>` so it is keyboard reachable and announced, which
8
+ * means the browser's default button chrome has to be stripped back to
9
+ * what the bare icon used to look like.
10
+ */
11
+
12
+ @layer byline-components {
13
+ .toggle,
14
+ :global(.byline-input-password-toggle) {
15
+ display: flex;
16
+ align-items: center;
17
+ justify-content: center;
18
+ padding: 0;
19
+ border: none;
20
+ background: none;
21
+ color: inherit;
22
+ cursor: pointer;
23
+ /* The icon is 18px; the box is padded out to a comfortable target
24
+ without changing where the icon sits. */
25
+ min-width: 1.375rem;
26
+ min-height: 1.375rem;
27
+ }
28
+
29
+ /*
30
+ * A hairline ring at reduced opacity. The control is a 22px box sitting
31
+ * inside a 36px field, so the 2px ring this started as, at full
32
+ * `--primary-500`, read as heavier than the input's own border and drew
33
+ * the eye away from the field. Thickness and offset both drop to 1px,
34
+ * which pulls the outer edge in by 4px without touching the button box —
35
+ * so the 18px icon and the click target are unchanged.
36
+ */
37
+ .toggle:focus-visible,
38
+ :global(.byline-input-password-toggle):focus-visible {
39
+ outline: 1px solid oklch(from var(--field-ring, var(--blue-500)) l c h / 0.6);
40
+ outline-offset: 1px;
41
+ border-radius: var(--border-radius-sm, 3px);
42
+ }
43
+ }
@@ -1,24 +1,66 @@
1
1
  import { useState } from 'react'
2
2
 
3
+ import cx from 'clsx'
4
+
3
5
  import { EyeClosedIcon } from '../../icons/eye-closed-icon.jsx'
4
6
  import { EyeOpenIcon } from '../../icons/eye-open-icon.jsx'
5
7
  import { Input } from './input.jsx'
8
+ import styles from './input-password.module.css'
6
9
  import type { InputProps } from './input.jsx'
7
10
 
8
- export function InputPassword({ ...props }: InputProps) {
11
+ /**
12
+ * `type` and `endAdornment` are the two props this component exists to
13
+ * control, so they are removed from the public surface rather than left
14
+ * to be overridden.
15
+ *
16
+ * They used to be overridable by accident: props were spread AFTER them,
17
+ * so a caller passing the obvious `type="password"` silently defeated the
18
+ * toggle — the icon swapped, the field stayed masked. Omitting them turns
19
+ * that into a compile error instead of a bug someone has to notice.
20
+ */
21
+ export interface InputPasswordProps extends Omit<InputProps, 'type' | 'endAdornment'> {
22
+ /** Accessible name for the reveal control while the password is hidden. */
23
+ showPasswordLabel?: string
24
+ /** Accessible name for the reveal control while the password is shown. */
25
+ hidePasswordLabel?: string
26
+ }
27
+
28
+ export function InputPassword({
29
+ showPasswordLabel = 'Show password',
30
+ hidePasswordLabel = 'Hide password',
31
+ ...props
32
+ }: InputPasswordProps) {
9
33
  const [visible, setVisible] = useState(false)
10
34
 
11
35
  return (
12
36
  <Input
37
+ // Spread first: `type` below is the whole point of the component and
38
+ // must win over anything a caller passes.
39
+ {...props}
13
40
  type={visible ? 'text' : 'password'}
14
41
  endAdornment={
15
- visible ? (
16
- <EyeOpenIcon width="18px" height="18px" onClick={() => setVisible(!visible)} />
17
- ) : (
18
- <EyeClosedIcon width="18px" height="18px" onClick={() => setVisible(!visible)} />
19
- )
42
+ <button
43
+ type="button"
44
+ className={cx('byline-input-password-toggle', styles.toggle)}
45
+ // A real button, not a div: the control has to be reachable by
46
+ // keyboard and announced by a screen reader. The icons are
47
+ // `aria-hidden`, so without a name here the control is silent.
48
+ aria-label={visible ? hidePasswordLabel : showPasswordLabel}
49
+ aria-pressed={visible}
50
+ // Clicking the toggle must not blur the field. Beyond keeping the
51
+ // caret where the user left it, a blur fires the form's validation
52
+ // and the re-render that follows can replace this element between
53
+ // mousedown and mouseup — which loses the click entirely.
54
+ onMouseDown={(event) => event.preventDefault()}
55
+ onClick={() => setVisible((current) => !current)}
56
+ >
57
+ {visible ? (
58
+ <EyeOpenIcon width="18px" height="18px" />
59
+ ) : (
60
+ <EyeClosedIcon width="18px" height="18px" />
61
+ )}
62
+ </button>
20
63
  }
21
- {...props}
22
64
  />
23
65
  )
24
66
  }