@estiva-app/ui 0.60.0 → 0.60.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estiva-app/ui",
3
- "version": "0.60.0",
3
+ "version": "0.60.2",
4
4
  "description": "Estiva's design tokens (the contract) and a small set of primitives (a convenience) for every Estiva app.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/registry.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "kind": "package",
5
5
  "repo": "estiva-ui",
6
6
  "package": "@estiva-app/ui",
7
- "packageVersion": "0.60.0",
7
+ "packageVersion": "0.60.2",
8
8
  "exports": 134,
9
9
  "typeExports": 141,
10
10
  "files": null
@@ -54,7 +54,9 @@ function Root({ children }) {
54
54
  did, which is also what a story or a test sees without one.
55
55
  - `pictureFor` gets the person's name and returns an address, or nothing for
56
56
  the initials.
57
- - `navigate` gets the link's `href` exactly as written. The browser keeps
57
+ - `navigate` gets the link's path, query and `#` part (`/message/1?x=2`),
58
+ also when the `href` is the app's whole address (`https://app.test/message/1`):
59
+ a router reads a whole address as a path under the current page. The browser keeps
58
60
  every other click: one already handled (`preventDefault`), one with Ctrl,
59
61
  Cmd, Shift or Alt, the middle button, a link that opens a new tab or
60
62
  downloads, and an address on another site or in another scheme
@@ -86,6 +86,13 @@ describe('changing page', () => {
86
86
  expect(navigate).toHaveBeenCalledWith('/topics')
87
87
  })
88
88
 
89
+ it("hands over the path when the href is the app's whole address", async () => {
90
+ const navigate = vi.fn()
91
+ inApp(navigate, <Link href={`${window.location.origin}/message/1?thread=2#r3`}>Message</Link>)
92
+ await userEvent.click(screen.getByRole('link'))
93
+ expect(navigate).toHaveBeenCalledWith('/message/1?thread=2#r3')
94
+ })
95
+
89
96
  it("runs the caller's own onClick first", async () => {
90
97
  const order: string[] = []
91
98
  inApp(() => order.push('navigate'), <Link href="/topics" onClick={() => order.push('caller')}>Topics</Link>)
@@ -10,8 +10,9 @@ export interface AppProviderProps {
10
10
  pictureFor?: (name: string) => string | null | undefined
11
11
  /**
12
12
  * How this app changes page without reloading. A plain click on a link to
13
- * this app's own address calls it with the link's `href` instead of loading
14
- * the page again: `Link` (and so `Card` and `Breadcrumb`'s crumbs),
13
+ * this app's own address calls it with the link's path (`/message/1`, also
14
+ * when the `href` is the whole `https://…` address) instead of loading the
15
+ * page again: `Link` (and so `Card` and `Breadcrumb`'s crumbs),
15
16
  * `NavItem` (and so `NavTree`'s rows), `RailItem`, `Chip`, `InlineChip`, and
16
17
  * `IconButton` (and so `ToolbarLink`).
17
18
  */
@@ -71,6 +72,8 @@ export function useInAppClick<E extends HTMLElement>(href: string | undefined, o
71
72
  }
72
73
  if (url.origin !== window.location.origin) return
73
74
  event.preventDefault()
74
- navigate(href)
75
+ // The path, never the whole address: a router reads `https://…` as a path
76
+ // under the current page and lands nowhere (Peek's message link, 8 October).
77
+ navigate(`${url.pathname}${url.search}${url.hash}`)
75
78
  }
76
79
  }
package/src/TopBar.mdx CHANGED
@@ -64,6 +64,8 @@ import { TopBar, IdentityMenu } from '@estiva-app/ui'
64
64
  don't restyle it per app.
65
65
  - `onSearch` is called on every Ctrl+K, open or not: to close the launcher
66
66
  on a second Ctrl+K, toggle in your handler.
67
+ - A click on the field leaves focus where it was, so a launcher can act on the
68
+ text box and the selection you were in. Tab still reaches the field.
67
69
  - The frame is desktop-only for now — there is no narrow-screen
68
70
  behaviour; the menu button collapses panels, it does not open a drawer.
69
71
 
@@ -29,6 +29,30 @@ describe('TopBar search', () => {
29
29
  expect(onSearch).toHaveBeenCalledTimes(1)
30
30
  })
31
31
 
32
+ it('leaves focus where it was on a click', async () => {
33
+ const onSearch = vi.fn()
34
+ render(
35
+ <>
36
+ <input aria-label="composer" />
37
+ <TopBar onSearch={onSearch} />
38
+ </>,
39
+ )
40
+ const composer = screen.getByRole('textbox', { name: 'composer' })
41
+ composer.focus()
42
+ await userEvent.click(screen.getByRole('button', { name: /Search…/ }))
43
+ expect(onSearch).toHaveBeenCalledTimes(1)
44
+ expect(document.activeElement).toBe(composer)
45
+ })
46
+
47
+ it('is reached with Tab and pressed with Enter', async () => {
48
+ const onSearch = vi.fn()
49
+ render(<TopBar onSearch={onSearch} />)
50
+ await userEvent.tab()
51
+ expect(document.activeElement).toBe(screen.getByRole('button', { name: /Search…/ }))
52
+ await userEvent.keyboard('{Enter}')
53
+ expect(onSearch).toHaveBeenCalledTimes(1)
54
+ })
55
+
32
56
  it('says "Search…" when given no words', () => {
33
57
  render(<TopBar onSearch={() => {}} />)
34
58
  expect(screen.getByRole('button', { name: /Search…/ })).toBeTruthy()
package/src/TopBar.tsx CHANGED
@@ -32,6 +32,11 @@ import { shortcutLabel } from './shortcuts'
32
32
  * app searches with — and Ctrl+K (Cmd+K on a Mac) presses it from anywhere.
33
33
  * Before, every app drew its own field and listened for the keys itself.
34
34
  * A `search` of the caller's own still takes the centre.
35
+ *
36
+ * **A click leaves focus where it was**, as Peek's field did: what searching
37
+ * opens often acts on what you were in — Peek's launcher reads the composer
38
+ * you were typing in and its selection — and taking focus would leave it
39
+ * nothing. Tab still reaches the field, and Enter or Space presses it.
35
40
  */
36
41
  export interface TopBarProps {
37
42
  variant?: 'solid' | 'floating' | 'inset'
@@ -73,7 +78,7 @@ export function TopBar({ variant = 'solid', menu, logo, search, onSearch, search
73
78
  const centre =
74
79
  search ??
75
80
  (onSearch && (
76
- <BaseButton type="button" onClick={onSearch} className={cn('flex w-[290px] cursor-pointer items-center gap-2 px-3 py-2', FIELD_SHELL_CLASSES)}>
81
+ <BaseButton type="button" onMouseDown={(event) => event.preventDefault()} onClick={onSearch} className={cn('flex w-[290px] cursor-pointer items-center gap-2 px-3 py-2', FIELD_SHELL_CLASSES)}>
77
82
  <span className="min-w-0 flex-1 truncate text-left text-input-value text-text-muted">{searchPlaceholder}</span>
78
83
  <Kbd>{shortcutLabel('Mod-K')}</Kbd>
79
84
  </BaseButton>