@happyvertical/smrt-ui 0.33.0 → 0.34.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.
|
@@ -63,21 +63,34 @@ const buttonOnlyProps = new Set([
|
|
|
63
63
|
'formTarget',
|
|
64
64
|
]);
|
|
65
65
|
|
|
66
|
-
//
|
|
66
|
+
// Attributes the disabled link sets itself. `rest` is spread *after* the
|
|
67
|
+
// explicit attributes on the <a>, so without excluding these a caller-supplied
|
|
68
|
+
// `tabindex` / `aria-disabled` in `rest` would win and make a "disabled" link
|
|
69
|
+
// focusable or announced as enabled again (PR #1602 review). Only stripped
|
|
70
|
+
// while disabled, so an enabled link still honors a caller's tabindex.
|
|
71
|
+
const disabledControlledProps = new Set(['tabindex', 'aria-disabled']);
|
|
72
|
+
|
|
73
|
+
// Filter out button-specific props (always) and the self-controlled disabled
|
|
74
|
+
// attributes (while disabled) for link mode.
|
|
67
75
|
const linkProps = $derived(() => {
|
|
68
76
|
return Object.fromEntries(
|
|
69
|
-
Object.entries(rest).filter(
|
|
77
|
+
Object.entries(rest).filter(
|
|
78
|
+
([key]) =>
|
|
79
|
+
!buttonOnlyProps.has(key) &&
|
|
80
|
+
!(isDisabled && disabledControlledProps.has(key)),
|
|
81
|
+
),
|
|
70
82
|
) as HTMLAnchorAttributes;
|
|
71
83
|
});
|
|
72
84
|
</script>
|
|
73
85
|
|
|
74
86
|
{#if isLink}
|
|
75
87
|
<a
|
|
76
|
-
{href}
|
|
88
|
+
href={isDisabled ? undefined : href}
|
|
77
89
|
class="button {variant} {size} {className}"
|
|
78
90
|
class:disabled={isDisabled}
|
|
79
91
|
class:full-width={fullWidth}
|
|
80
92
|
class:loading
|
|
93
|
+
tabindex={isDisabled ? -1 : undefined}
|
|
81
94
|
aria-disabled={isDisabled}
|
|
82
95
|
aria-busy={loading}
|
|
83
96
|
onclick={onclick as HTMLAnchorAttributes['onclick']}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Button.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/ui/Button.svelte.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAEV,oBAAoB,EACrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGrE,iCAAiC;AACjC,MAAM,WAAW,KAAM,SAAQ,IAAI,CAAC,oBAAoB,EAAE,OAAO,GAAG,MAAM,CAAC;IACzE,qBAAqB;IACrB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,mBAAmB;IACnB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wBAAwB;IACxB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oBAAoB;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;
|
|
1
|
+
{"version":3,"file":"Button.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/ui/Button.svelte.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAEV,oBAAoB,EACrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGrE,iCAAiC;AACjC,MAAM,WAAW,KAAM,SAAQ,IAAI,CAAC,oBAAoB,EAAE,OAAO,GAAG,MAAM,CAAC;IACzE,qBAAqB;IACrB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,mBAAmB;IACnB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wBAAwB;IACxB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oBAAoB;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAiFD,QAAA,MAAM,MAAM,2CAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
|
|
@@ -83,6 +83,50 @@ describe('Button', () => {
|
|
|
83
83
|
expect(link).toHaveClass('nav-link');
|
|
84
84
|
expect(link).toHaveClass('button');
|
|
85
85
|
});
|
|
86
|
+
it('is not navigable when disabled in link mode', async () => {
|
|
87
|
+
const { container } = render(Button, {
|
|
88
|
+
props: { children: textSnippet('Home'), href: '/home', disabled: true },
|
|
89
|
+
});
|
|
90
|
+
// A disabled link-mode Button must be genuinely non-navigable: it drops its
|
|
91
|
+
// href (no navigation target), leaves the tab order, and is announced as
|
|
92
|
+
// disabled. Previously it kept a live href + tab stop and only blocked the
|
|
93
|
+
// mouse via CSS pointer-events, so keyboard/AT users could still activate
|
|
94
|
+
// it (flagged by the PR #1599 auto-review).
|
|
95
|
+
const anchor = container.querySelector('a');
|
|
96
|
+
if (anchor === null)
|
|
97
|
+
throw new Error('expected a rendered <a> in link mode');
|
|
98
|
+
expect(anchor).not.toHaveAttribute('href');
|
|
99
|
+
expect(anchor).toHaveAttribute('tabindex', '-1');
|
|
100
|
+
expect(anchor).toHaveAttribute('aria-disabled', 'true');
|
|
101
|
+
// No href => no implicit link role => not reachable as a link by AT.
|
|
102
|
+
expect(screen.queryByRole('link')).toBeNull();
|
|
103
|
+
// ...and keyboard focus can't land on it, so Enter can't navigate.
|
|
104
|
+
await userEvent.tab();
|
|
105
|
+
expect(anchor).not.toHaveFocus();
|
|
106
|
+
await expectNoA11yViolations(container);
|
|
107
|
+
});
|
|
108
|
+
it('keeps a disabled link non-navigable even when the caller passes tabindex/aria-disabled', async () => {
|
|
109
|
+
const { container } = render(Button, {
|
|
110
|
+
props: {
|
|
111
|
+
children: textSnippet('Home'),
|
|
112
|
+
href: '/home',
|
|
113
|
+
disabled: true,
|
|
114
|
+
// Passthrough props that must NOT win over the disabled semantics:
|
|
115
|
+
// `rest` is spread after the component's own attributes on the <a>.
|
|
116
|
+
tabindex: 0,
|
|
117
|
+
'aria-disabled': false,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
const anchor = container.querySelector('a');
|
|
121
|
+
if (anchor === null)
|
|
122
|
+
throw new Error('expected a rendered <a> in link mode');
|
|
123
|
+
expect(anchor).toHaveAttribute('tabindex', '-1');
|
|
124
|
+
expect(anchor).toHaveAttribute('aria-disabled', 'true');
|
|
125
|
+
expect(anchor).not.toHaveAttribute('href');
|
|
126
|
+
await userEvent.tab();
|
|
127
|
+
expect(anchor).not.toHaveFocus();
|
|
128
|
+
await expectNoA11yViolations(container);
|
|
129
|
+
});
|
|
86
130
|
it('is axe-clean', async () => {
|
|
87
131
|
const { container } = render(Button, {
|
|
88
132
|
props: { children: textSnippet('Accessible') },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Domain-agnostic Svelte 5 UI runtime for SMRT: primitives, i18n client, theme system, and module UI registry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
},
|
|
109
109
|
"dependencies": {
|
|
110
110
|
"esm-env": "^1.2.2",
|
|
111
|
-
"@happyvertical/smrt-types": "0.
|
|
111
|
+
"@happyvertical/smrt-types": "0.34.0"
|
|
112
112
|
},
|
|
113
113
|
"peerDependencies": {
|
|
114
114
|
"svelte": "^5.18.2"
|