@happyvertical/smrt-ui 0.33.0 → 0.33.1

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
- // Filter out button-specific props for link mode
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(([key]) => !buttonOnlyProps.has(key)),
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;AAqED,QAAA,MAAM,MAAM,2CAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
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.33.0",
3
+ "version": "0.33.1",
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.33.0"
111
+ "@happyvertical/smrt-types": "0.33.1"
112
112
  },
113
113
  "peerDependencies": {
114
114
  "svelte": "^5.18.2"