@open-mercato/ui 0.7.1-develop.7148.1.3076e5ccf7 → 0.7.1-develop.7149.1.7efa6e1612
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.
|
@@ -28,51 +28,59 @@ function ViewChip({
|
|
|
28
28
|
const t = useT();
|
|
29
29
|
if (isRenaming) {
|
|
30
30
|
const renameTrimmed = renameValue.trim();
|
|
31
|
-
return /* @__PURE__ */ jsxs(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
31
|
+
return /* @__PURE__ */ jsxs(
|
|
32
|
+
"div",
|
|
33
|
+
{
|
|
34
|
+
"data-active": isActive ? "true" : "false",
|
|
35
|
+
className: "inline-flex items-center gap-1 rounded-md border border-primary bg-primary/5 px-3 h-8",
|
|
36
|
+
children: [
|
|
37
|
+
/* @__PURE__ */ jsx(
|
|
38
|
+
"input",
|
|
39
|
+
{
|
|
40
|
+
value: renameValue,
|
|
41
|
+
onChange: (e) => onRenameValueChange(e.target.value),
|
|
42
|
+
className: "w-24 bg-transparent text-sm outline-none",
|
|
43
|
+
autoFocus: true,
|
|
44
|
+
onKeyDown: (e) => {
|
|
45
|
+
if (e.key === "Enter") onRenameConfirm();
|
|
46
|
+
if (e.key === "Escape") onRenameCancel();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
),
|
|
50
|
+
/* @__PURE__ */ jsx(
|
|
51
|
+
IconButton,
|
|
52
|
+
{
|
|
53
|
+
type: "button",
|
|
54
|
+
variant: "ghost",
|
|
55
|
+
size: "xs",
|
|
56
|
+
className: "h-auto",
|
|
57
|
+
onClick: onRenameConfirm,
|
|
58
|
+
disabled: !renameTrimmed,
|
|
59
|
+
"aria-label": t("ui.perspectives.rename.confirm", "Confirm rename"),
|
|
60
|
+
children: /* @__PURE__ */ jsx(Check, { className: `size-4 ${renameTrimmed ? "text-brand-violet" : "text-muted-foreground opacity-50"}` })
|
|
61
|
+
}
|
|
62
|
+
),
|
|
63
|
+
/* @__PURE__ */ jsx(
|
|
64
|
+
IconButton,
|
|
65
|
+
{
|
|
66
|
+
type: "button",
|
|
67
|
+
variant: "ghost",
|
|
68
|
+
size: "xs",
|
|
69
|
+
className: "h-auto",
|
|
70
|
+
onClick: onRenameCancel,
|
|
71
|
+
"aria-label": t("ui.perspectives.rename.cancel", "Cancel rename"),
|
|
72
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-4 text-muted-foreground hover:text-destructive" })
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
);
|
|
71
78
|
}
|
|
72
79
|
const showUsersIcon = kind === "role" || isShared;
|
|
73
80
|
return /* @__PURE__ */ jsxs(
|
|
74
81
|
"div",
|
|
75
82
|
{
|
|
83
|
+
"data-active": isActive ? "true" : "false",
|
|
76
84
|
className: `inline-flex items-center gap-0.5 rounded-md border h-8 text-sm ${isActive ? "bg-brand-violet/10 border-brand-violet/30 font-medium text-brand-violet" : "border-border"}`,
|
|
77
85
|
children: [
|
|
78
86
|
/* @__PURE__ */ jsxs(
|
|
@@ -84,6 +92,7 @@ function ViewChip({
|
|
|
84
92
|
className: "h-auto px-3 py-0 text-sm hover:bg-transparent",
|
|
85
93
|
onClick: onActivate,
|
|
86
94
|
disabled,
|
|
95
|
+
"aria-current": isActive ? "true" : void 0,
|
|
87
96
|
children: [
|
|
88
97
|
showUsersIcon ? /* @__PURE__ */ jsx(Users, { className: "size-3 mr-1 opacity-50" }) : null,
|
|
89
98
|
label
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/views/ViewChip.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Check, X, Users } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { ViewChipMenu } from './ViewChipMenu'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype Props = {\n id: string\n label: string\n kind: 'personal' | 'role'\n isActive: boolean\n disabled: boolean\n isShared: boolean\n isRenaming: boolean\n renameValue: string\n canApplyToRoles: boolean\n deleting: boolean\n onActivate: () => void\n onRenameValueChange: (value: string) => void\n onRenameConfirm: () => void\n onRenameCancel: () => void\n onRenameStart?: () => void\n onClone: () => void\n onShareStart?: () => void\n onDelete: () => void\n}\n\nexport function ViewChip({\n id,\n label,\n kind,\n isActive,\n disabled,\n isShared,\n isRenaming,\n renameValue,\n canApplyToRoles,\n deleting,\n onActivate,\n onRenameValueChange,\n onRenameConfirm,\n onRenameCancel,\n onRenameStart,\n onClone,\n onShareStart,\n onDelete,\n}: Props) {\n const t = useT()\n\n if (isRenaming) {\n const renameTrimmed = renameValue.trim()\n return (\n <div className=\"inline-flex items-center gap-1 rounded-md border border-primary bg-primary/5 px-3 h-8\">\n <input\n value={renameValue}\n onChange={(e) => onRenameValueChange(e.target.value)}\n className=\"w-24 bg-transparent text-sm outline-none\"\n autoFocus\n onKeyDown={(e) => {\n if (e.key === 'Enter') onRenameConfirm()\n if (e.key === 'Escape') onRenameCancel()\n }}\n />\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n className=\"h-auto\"\n onClick={onRenameConfirm}\n disabled={!renameTrimmed}\n aria-label={t('ui.perspectives.rename.confirm', 'Confirm rename')}\n >\n <Check className={`size-4 ${renameTrimmed ? 'text-brand-violet' : 'text-muted-foreground opacity-50'}`} />\n </IconButton>\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n className=\"h-auto\"\n onClick={onRenameCancel}\n aria-label={t('ui.perspectives.rename.cancel', 'Cancel rename')}\n >\n <X className=\"size-4 text-muted-foreground hover:text-destructive\" />\n </IconButton>\n </div>\n )\n }\n\n const showUsersIcon = kind === 'role' || isShared\n\n return (\n <div\n className={`inline-flex items-center gap-0.5 rounded-md border h-8 text-sm ${\n isActive\n ? 'bg-brand-violet/10 border-brand-violet/30 font-medium text-brand-violet'\n : 'border-border'\n }`}\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-auto px-3 py-0 text-sm hover:bg-transparent\"\n onClick={onActivate}\n disabled={disabled}\n >\n {showUsersIcon ? <Users className=\"size-3 mr-1 opacity-50\" /> : null}\n {label}\n </Button>\n <ViewChipMenu\n kind={kind}\n canApplyToRoles={canApplyToRoles}\n deleting={deleting}\n onRenameStart={onRenameStart}\n onClone={onClone}\n onShareStart={onShareStart}\n onDelete={onDelete}\n />\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";AAsDM,
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Check, X, Users } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { ViewChipMenu } from './ViewChipMenu'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype Props = {\n id: string\n label: string\n kind: 'personal' | 'role'\n isActive: boolean\n disabled: boolean\n isShared: boolean\n isRenaming: boolean\n renameValue: string\n canApplyToRoles: boolean\n deleting: boolean\n onActivate: () => void\n onRenameValueChange: (value: string) => void\n onRenameConfirm: () => void\n onRenameCancel: () => void\n onRenameStart?: () => void\n onClone: () => void\n onShareStart?: () => void\n onDelete: () => void\n}\n\nexport function ViewChip({\n id,\n label,\n kind,\n isActive,\n disabled,\n isShared,\n isRenaming,\n renameValue,\n canApplyToRoles,\n deleting,\n onActivate,\n onRenameValueChange,\n onRenameConfirm,\n onRenameCancel,\n onRenameStart,\n onClone,\n onShareStart,\n onDelete,\n}: Props) {\n const t = useT()\n\n if (isRenaming) {\n const renameTrimmed = renameValue.trim()\n return (\n <div\n data-active={isActive ? 'true' : 'false'}\n className=\"inline-flex items-center gap-1 rounded-md border border-primary bg-primary/5 px-3 h-8\"\n >\n <input\n value={renameValue}\n onChange={(e) => onRenameValueChange(e.target.value)}\n className=\"w-24 bg-transparent text-sm outline-none\"\n autoFocus\n onKeyDown={(e) => {\n if (e.key === 'Enter') onRenameConfirm()\n if (e.key === 'Escape') onRenameCancel()\n }}\n />\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n className=\"h-auto\"\n onClick={onRenameConfirm}\n disabled={!renameTrimmed}\n aria-label={t('ui.perspectives.rename.confirm', 'Confirm rename')}\n >\n <Check className={`size-4 ${renameTrimmed ? 'text-brand-violet' : 'text-muted-foreground opacity-50'}`} />\n </IconButton>\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n className=\"h-auto\"\n onClick={onRenameCancel}\n aria-label={t('ui.perspectives.rename.cancel', 'Cancel rename')}\n >\n <X className=\"size-4 text-muted-foreground hover:text-destructive\" />\n </IconButton>\n </div>\n )\n }\n\n const showUsersIcon = kind === 'role' || isShared\n\n return (\n <div\n data-active={isActive ? 'true' : 'false'}\n className={`inline-flex items-center gap-0.5 rounded-md border h-8 text-sm ${\n isActive\n ? 'bg-brand-violet/10 border-brand-violet/30 font-medium text-brand-violet'\n : 'border-border'\n }`}\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-auto px-3 py-0 text-sm hover:bg-transparent\"\n onClick={onActivate}\n disabled={disabled}\n aria-current={isActive ? 'true' : undefined}\n >\n {showUsersIcon ? <Users className=\"size-3 mr-1 opacity-50\" /> : null}\n {label}\n </Button>\n <ViewChipMenu\n kind={kind}\n canApplyToRoles={canApplyToRoles}\n deleting={deleting}\n onRenameStart={onRenameStart}\n onClone={onClone}\n onShareStart={onShareStart}\n onDelete={onDelete}\n />\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAsDM,SAIE,KAJF;AApDN,SAAS,OAAO,GAAG,aAAa;AAChC,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,YAAY;AAuBd,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAU;AACR,QAAM,IAAI,KAAK;AAEf,MAAI,YAAY;AACd,UAAM,gBAAgB,YAAY,KAAK;AACvC,WACE;AAAA,MAAC;AAAA;AAAA,QACC,eAAa,WAAW,SAAS;AAAA,QACjC,WAAU;AAAA,QAEV;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,UAAU,CAAC,MAAM,oBAAoB,EAAE,OAAO,KAAK;AAAA,cACnD,WAAU;AAAA,cACV,WAAS;AAAA,cACT,WAAW,CAAC,MAAM;AAChB,oBAAI,EAAE,QAAQ,QAAS,iBAAgB;AACvC,oBAAI,EAAE,QAAQ,SAAU,gBAAe;AAAA,cACzC;AAAA;AAAA,UACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,WAAU;AAAA,cACV,SAAS;AAAA,cACT,UAAU,CAAC;AAAA,cACX,cAAY,EAAE,kCAAkC,gBAAgB;AAAA,cAEhE,8BAAC,SAAM,WAAW,UAAU,gBAAgB,sBAAsB,kCAAkC,IAAI;AAAA;AAAA,UAC1G;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,WAAU;AAAA,cACV,SAAS;AAAA,cACT,cAAY,EAAE,iCAAiC,eAAe;AAAA,cAE9D,8BAAC,KAAE,WAAU,uDAAsD;AAAA;AAAA,UACrE;AAAA;AAAA;AAAA,IACF;AAAA,EAEJ;AAEA,QAAM,gBAAgB,SAAS,UAAU;AAEzC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAa,WAAW,SAAS;AAAA,MACjC,WAAW,kEACT,WACI,4EACA,eACN;AAAA,MAEA;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,WAAU;AAAA,YACV,SAAS;AAAA,YACT;AAAA,YACA,gBAAc,WAAW,SAAS;AAAA,YAEjC;AAAA,8BAAgB,oBAAC,SAAM,WAAU,0BAAyB,IAAK;AAAA,cAC/D;AAAA;AAAA;AAAA,QACH;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA;AAAA,QACF;AAAA;AAAA;AAAA,EACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7149.1.7efa6e1612",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -155,14 +155,14 @@
|
|
|
155
155
|
"remark-gfm": "^4.0.1"
|
|
156
156
|
},
|
|
157
157
|
"peerDependencies": {
|
|
158
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
158
|
+
"@open-mercato/shared": "0.7.1-develop.7149.1.7efa6e1612",
|
|
159
159
|
"react": ">=18.0.0",
|
|
160
160
|
"react-dom": ">=18.0.0",
|
|
161
161
|
"react-is": ">=18.0.0"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|
|
164
164
|
"@figma/code-connect": "^1.3.4",
|
|
165
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
165
|
+
"@open-mercato/shared": "0.7.1-develop.7149.1.7efa6e1612",
|
|
166
166
|
"@testing-library/dom": "^10.4.1",
|
|
167
167
|
"@testing-library/jest-dom": "^7.0.0",
|
|
168
168
|
"@testing-library/react": "^16.3.1",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
import { screen } from '@testing-library/react'
|
|
7
|
+
import { ViewChip } from '../views/ViewChip'
|
|
8
|
+
import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWithProviders'
|
|
9
|
+
|
|
10
|
+
// Coverage for issue #5846: the chip's "this view is active" state used to live
|
|
11
|
+
// only in its Tailwind classes (`bg-brand-violet/10 border-brand-violet/30 ...`),
|
|
12
|
+
// so the only way to assert it was to string-match `className` on `.parentElement`.
|
|
13
|
+
// That broke on any restyle and left the state invisible to assistive technology.
|
|
14
|
+
// The chip now carries `data-active` on its root and `aria-current` on the button
|
|
15
|
+
// the user actually activates, mirroring how `pagination.tsx` marks the current page.
|
|
16
|
+
|
|
17
|
+
type ChipProps = React.ComponentProps<typeof ViewChip>
|
|
18
|
+
|
|
19
|
+
function renderChip(overrides: Partial<ChipProps> = {}) {
|
|
20
|
+
const props: ChipProps = {
|
|
21
|
+
id: 'view-1',
|
|
22
|
+
label: 'My view',
|
|
23
|
+
kind: 'personal',
|
|
24
|
+
isActive: false,
|
|
25
|
+
disabled: false,
|
|
26
|
+
isShared: false,
|
|
27
|
+
isRenaming: false,
|
|
28
|
+
renameValue: '',
|
|
29
|
+
canApplyToRoles: false,
|
|
30
|
+
deleting: false,
|
|
31
|
+
onActivate: jest.fn(),
|
|
32
|
+
onRenameValueChange: jest.fn(),
|
|
33
|
+
onRenameConfirm: jest.fn(),
|
|
34
|
+
onRenameCancel: jest.fn(),
|
|
35
|
+
onClone: jest.fn(),
|
|
36
|
+
onDelete: jest.fn(),
|
|
37
|
+
...overrides,
|
|
38
|
+
}
|
|
39
|
+
return renderWithProviders(<ViewChip {...props} />, { dict: {} })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('ViewChip active-state hook', () => {
|
|
43
|
+
it('marks the active chip with data-active="true" and aria-current on its activate button', () => {
|
|
44
|
+
const { container } = renderChip({ isActive: true, label: 'Active view' })
|
|
45
|
+
|
|
46
|
+
expect(container.querySelector('[data-active="true"]')).toBeInTheDocument()
|
|
47
|
+
expect(screen.getByRole('button', { name: 'Active view' })).toHaveAttribute('aria-current', 'true')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('marks an inactive chip with data-active="false" and omits aria-current entirely', () => {
|
|
51
|
+
const { container } = renderChip({ isActive: false, label: 'Inactive view' })
|
|
52
|
+
|
|
53
|
+
expect(container.querySelector('[data-active="false"]')).toBeInTheDocument()
|
|
54
|
+
expect(container.querySelector('[data-active="true"]')).toBeNull()
|
|
55
|
+
expect(screen.getByRole('button', { name: 'Inactive view' })).not.toHaveAttribute('aria-current')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('exposes the active state without depending on the chip styling', () => {
|
|
59
|
+
const { container } = renderChip({ isActive: true, label: 'Styled view' })
|
|
60
|
+
|
|
61
|
+
const chip = container.querySelector('[data-active]') as HTMLElement
|
|
62
|
+
expect(chip).toBeInTheDocument()
|
|
63
|
+
|
|
64
|
+
// Strip every class off the chip: the semantic hook must survive a restyle
|
|
65
|
+
// that drops or renames the `border-brand-violet/30` marker the old assertion read.
|
|
66
|
+
chip.className = ''
|
|
67
|
+
expect(chip.getAttribute('data-active')).toBe('true')
|
|
68
|
+
expect(screen.getByRole('button', { name: 'Styled view' })).toHaveAttribute('aria-current', 'true')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('keeps the hook on the chip root while the view is being renamed', () => {
|
|
72
|
+
const { container } = renderChip({ isActive: true, isRenaming: true, renameValue: 'Draft name' })
|
|
73
|
+
|
|
74
|
+
expect(container.querySelector('[data-active="true"]')).toBeInTheDocument()
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -52,7 +52,10 @@ export function ViewChip({
|
|
|
52
52
|
if (isRenaming) {
|
|
53
53
|
const renameTrimmed = renameValue.trim()
|
|
54
54
|
return (
|
|
55
|
-
<div
|
|
55
|
+
<div
|
|
56
|
+
data-active={isActive ? 'true' : 'false'}
|
|
57
|
+
className="inline-flex items-center gap-1 rounded-md border border-primary bg-primary/5 px-3 h-8"
|
|
58
|
+
>
|
|
56
59
|
<input
|
|
57
60
|
value={renameValue}
|
|
58
61
|
onChange={(e) => onRenameValueChange(e.target.value)}
|
|
@@ -92,6 +95,7 @@ export function ViewChip({
|
|
|
92
95
|
|
|
93
96
|
return (
|
|
94
97
|
<div
|
|
98
|
+
data-active={isActive ? 'true' : 'false'}
|
|
95
99
|
className={`inline-flex items-center gap-0.5 rounded-md border h-8 text-sm ${
|
|
96
100
|
isActive
|
|
97
101
|
? 'bg-brand-violet/10 border-brand-violet/30 font-medium text-brand-violet'
|
|
@@ -105,6 +109,7 @@ export function ViewChip({
|
|
|
105
109
|
className="h-auto px-3 py-0 text-sm hover:bg-transparent"
|
|
106
110
|
onClick={onActivate}
|
|
107
111
|
disabled={disabled}
|
|
112
|
+
aria-current={isActive ? 'true' : undefined}
|
|
108
113
|
>
|
|
109
114
|
{showUsersIcon ? <Users className="size-3 mr-1 opacity-50" /> : null}
|
|
110
115
|
{label}
|