@rcarls/rc-fab 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -1,8 +1,16 @@
1
1
  # `@rcarls/rc-fab`
2
2
 
3
- A floating action button with regular icon-only and extended icon-and-label
4
- variants. The component uses a native shadow-DOM `<button>` and provides no
5
- positioning behavior; consumers own where the action floats in their layout.
3
+ Sticky floating action button modeled after the [Material 3 Floating action button](https://m3.material.io/components/floating-action-button/overview), wrapping a consumer-supplied button with scroll-aware visibility.
4
+
5
+ Docs: [https://richardcarls.github.io/rc-webcomponents/components/rc-fab](https://richardcarls.github.io/rc-webcomponents/components/rc-fab).
6
+
7
+ Use this for "back to top", sticky CTAs, chat launchers, and of course as FABs in your
8
+ Material Design website or PWA.
9
+
10
+ Place a native `<button>` as the direct child. The button's own accessible name (text
11
+ content or `aria-label`) becomes the FAB's accessible name.
12
+
13
+ Icons go inside the button alongside or instead of visible text.
6
14
 
7
15
  ## Installation
8
16
 
@@ -17,21 +25,70 @@ import '@rcarls/rc-fab/define';
17
25
  ## Usage
18
26
 
19
27
  ```html
20
- <rc-fab label="New recipe">
21
- <span slot="icon" aria-hidden="true">+</span>
28
+ <!-- Back to top (scroll-triggered) -->
29
+ <rc-fab scroll-reveal>
30
+ <button type="button" aria-label="Back to top"
31
+ onclick="scrollTo({ top: 0, behavior: 'smooth' })">
32
+ <span aria-hidden="true">↑</span>
33
+ </button>
22
34
  </rc-fab>
23
35
 
24
- <rc-fab variant="extended" label="New recipe">
25
- <span slot="icon" aria-hidden="true">+</span>
36
+ <!-- Always-visible action -->
37
+ <rc-fab>
38
+ <button type="button" aria-label="Create">
39
+ <span class="material-symbols-outlined" aria-hidden="true">add</span>
40
+ </button>
41
+ </rc-fab>
42
+
43
+ <!-- Extended FAB (icon + visible label) -->
44
+ <rc-fab>
45
+ <button type="button">
46
+ <span class="material-symbols-outlined" aria-hidden="true">edit</span>
47
+ Compose
48
+ </button>
26
49
  </rc-fab>
27
50
  ```
28
51
 
29
- `label` is required for an accessible name. It is visually hidden in the
30
- regular variant and visible in the extended variant.
52
+ ## Key attributes and CSS custom properties
53
+
54
+ | Attribute / property | Description |
55
+ | --- | --- |
56
+ | `position` | Viewport corner: `bottom-end` (default), `bottom-start`, `top-end`, `top-start` |
57
+ | `scroll-reveal` | Reveal the FAB only after scrolling past `--rc-fab-scroll-threshold` |
58
+
59
+ | CSS custom property | Default | Description |
60
+ | --- | --- | --- |
61
+ | `--rc-fab-position` | `fixed` | CSS position value. Use `absolute` for layout-relative placement |
62
+ | `--rc-fab-scroll-threshold` | `300px` | Scroll distance before the FAB appears (requires `scroll-reveal`) |
63
+ | `--rc-fab-scroll-timeline` | `scroll(root block)` | Override scroll target for the CSS animation path (e.g. `scroll(nearest block)`) |
64
+ | `--rc-fab-radius` | `9999px` | Border-radius. Default is pill-shaped. Use `50%` for a circle or `1rem` for Material rounded-square |
65
+ | `--rc-fab-size` | `3.5rem` | Height and minimum width |
66
+ | `--rc-fab-bg` | `ButtonFace` | Button background colour |
67
+ | `--rc-fab-color` | `ButtonText` | Button foreground colour |
68
+ | `--rc-fab-shadow` | `none` | Elevation shadow |
69
+ | `--rc-fab-inset-block` | `1.5rem` | Distance from the block-axis edge |
70
+ | `--rc-fab-inset-inline` | `1.5rem` | Distance from the inline-axis edge |
71
+
72
+ ## Scroll-reveal behaviour
73
+
74
+ `scroll-reveal` uses CSS scroll-driven animations (`animation-timeline: scroll()`).
75
+
76
+ In browsers that do not yet support this API, a passive scroll listener provides
77
+ equivalent behaviour.
78
+
79
+ The `[scroll-below-threshold]` attribute is toggled on the host and CSS transitions
80
+ handle the show/hide. The FAB is always visible in any remaining edge cases.
81
+
82
+ When the button receives `focus-visible` while below the threshold the animation is
83
+ overridden and the FAB is shown fully.
31
84
 
32
85
  ## Accessibility
33
86
 
34
- - The inner control is a native `<button type="button">`.
35
- - `disabled` maps to the native button's disabled state.
87
+ - The button's `aria-label` or text content is its accessible name.
36
88
  - Decorative icon content should use `aria-hidden="true"`.
37
- - The component does not create a landmark or choose a page position.
89
+ - The component does not choose a semantic landmark role or page position.
90
+ - With `scroll-reveal`, the FAB is removed from the accessibility tree and tab
91
+ order while hidden, matching the expectation that invisible controls are
92
+ unreachable.
93
+ - The `50%` point in the reveal animation lifts `visibility: hidden` early so
94
+ keyboard users can reach the button as it starts fading in.
@@ -46,13 +46,33 @@
46
46
  "declarations": [
47
47
  {
48
48
  "kind": "class",
49
- "description": "A Material Design 3 Floating Action Button.\n\nUse `variant=\"regular\"` (default) for an icon-only FAB, or\n`variant=\"extended\"` to show an icon alongside the `label`.\n\nPlace the button icon in the `icon` slot:\n```html\n<rc-fab label=\"New recipe\">\n <iconify-icon slot=\"icon\" icon=\"mdi-plus\"></iconify-icon>\n</rc-fab>\n```",
49
+ "description": "Sticky floating action button modeled after the Material 3 Floating action button,\nwrapping a consumer-supplied button with scroll-aware visibility.\n\nAdapted from the Material Design FAB component, use this for \"back to top\",\nsticky CTAs, chat launchers, and of course as FABs in your Material Design PWA.\n\nPlace a native `<button>` as the direct child. The button's own accessible\nname (text content or `aria-label`) becomes the FAB's accessible name.\n\nIcons go inside the button alongside or instead of visible text.",
50
50
  "name": "RCFab",
51
51
  "cssProperties": [
52
+ {
53
+ "description": "CSS position value. Override to `absolute` for layout-relative placement or `sticky` for scroll-snapping.",
54
+ "name": "--rc-fab-position",
55
+ "default": "fixed"
56
+ },
57
+ {
58
+ "description": "Distance from the block-axis edge.",
59
+ "name": "--rc-fab-inset-block",
60
+ "default": "1.5rem"
61
+ },
62
+ {
63
+ "description": "Distance from the inline-axis edge.",
64
+ "name": "--rc-fab-inset-inline",
65
+ "default": "1.5rem"
66
+ },
67
+ {
68
+ "description": "Stacking order.",
69
+ "name": "--rc-fab-z-index",
70
+ "default": "10"
71
+ },
52
72
  {
53
73
  "description": "Button background colour.",
54
74
  "name": "--rc-fab-bg",
55
- "default": "Canvas"
75
+ "default": "ButtonFace"
56
76
  },
57
77
  {
58
78
  "description": "Hover background colour.",
@@ -60,59 +80,62 @@
60
80
  "default": "var(--rc-fab-bg)"
61
81
  },
62
82
  {
63
- "description": "Foreground (icon + label) colour.",
83
+ "description": "Button foreground colour.",
64
84
  "name": "--rc-fab-color",
65
- "default": "CanvasText"
85
+ "default": "ButtonText"
66
86
  },
67
87
  {
68
- "description": "Height and minimum width of the button.",
88
+ "description": "Height and minimum width.",
69
89
  "name": "--rc-fab-size",
70
90
  "default": "3.5rem"
71
91
  },
72
92
  {
73
- "description": "Border-radius of the button.",
93
+ "description": "Border-radius. Default is pill-shaped. Override to `50%` for a circle (icon-only), `1rem` for Material rounded-square, etc.",
74
94
  "name": "--rc-fab-radius",
75
- "default": "1rem"
95
+ "default": "9999px"
76
96
  },
77
97
  {
78
- "description": "Box shadow at rest.",
79
- "name": "--rc-fab-shadow"
98
+ "description": "Elevation shadow.",
99
+ "name": "--rc-fab-shadow",
100
+ "default": "none"
80
101
  },
81
102
  {
82
- "description": "Box shadow on hover.",
83
- "name": "--rc-fab-shadow-hover"
103
+ "description": "Hover shadow.",
104
+ "name": "--rc-fab-shadow-hover",
105
+ "default": "var(--rc-fab-shadow)"
84
106
  },
85
107
  {
86
- "description": "Box shadow while pressed.",
87
- "name": "--rc-fab-shadow-active"
108
+ "description": "Pressed shadow.",
109
+ "name": "--rc-fab-shadow-active",
110
+ "default": "none"
88
111
  },
89
112
  {
90
- "description": "Inline padding (extended variant).",
113
+ "description": "Inline padding.",
91
114
  "name": "--rc-fab-padding-inline",
92
115
  "default": "1rem"
93
116
  },
94
117
  {
95
- "description": "Gap between icon and label (extended variant).",
118
+ "description": "Gap between icon and label text.",
96
119
  "name": "--rc-fab-gap",
97
120
  "default": "0.5rem"
98
121
  },
99
122
  {
100
- "description": "Font-size applied to the slotted icon.",
101
- "name": "--rc-fab-icon-size",
102
- "default": "1.5rem"
123
+ "description": "Font family for label text.",
124
+ "name": "--rc-fab-font-family",
125
+ "default": "inherit"
103
126
  },
104
127
  {
105
- "description": "Label font size (extended variant).",
128
+ "description": "Font size for label text.",
106
129
  "name": "--rc-fab-font-size",
107
130
  "default": "0.875rem"
108
131
  },
109
132
  {
110
- "description": "Label font weight.",
133
+ "description": "Font weight for label text.",
111
134
  "name": "--rc-fab-font-weight",
112
135
  "default": "500"
113
136
  },
114
137
  {
115
- "description": "Label letter spacing.",
138
+ "description": "Letter spacing for label text.",
116
139
  "name": "--rc-fab-letter-spacing",
117
140
  "default": "0.00625em"
118
141
  },
@@ -127,84 +150,102 @@
127
150
  "default": "2px"
128
151
  },
129
152
  {
130
- "description": "Transition speed for hover/active.",
153
+ "description": "Opacity applied when the button is disabled.",
154
+ "name": "--rc-fab-disabled-opacity",
155
+ "default": "0.38"
156
+ },
157
+ {
158
+ "description": "Transition speed for hover and active states.",
131
159
  "name": "--rc-fab-transition-duration",
132
160
  "default": "200ms"
133
- }
134
- ],
135
- "cssParts": [
161
+ },
162
+ {
163
+ "description": "Scroll distance at which the FAB becomes fully visible. Requires the `scroll-reveal` attribute. The JS fallback reads this value once on connect; px units only.",
164
+ "name": "--rc-fab-scroll-threshold",
165
+ "default": "300px"
166
+ },
136
167
  {
137
- "description": "The inner `<button>` element.",
138
- "name": "button"
168
+ "description": "The `animation-timeline` value used for scroll-reveal. Override to target a different scroller, e.g. `scroll(nearest block)` for embedded contexts. CSS path only; the JS fallback discovers the nearest scrollable ancestor automatically.",
169
+ "name": "--rc-fab-scroll-timeline",
170
+ "default": "scroll(root block)"
139
171
  }
140
172
  ],
141
173
  "slots": [
142
174
  {
143
- "description": "The icon element displayed inside the button.",
144
- "name": "icon"
175
+ "description": "The native `<button>` element. The button's own accessible name (text content or `aria-label`) serves as the FAB's accessible name.",
176
+ "name": "default"
145
177
  }
146
178
  ],
147
179
  "members": [
148
180
  {
149
181
  "kind": "field",
150
- "name": "variant",
182
+ "name": "_scrollCtrl",
151
183
  "type": {
152
- "text": "'regular' | 'extended'"
184
+ "text": "ScrollObserverController | undefined"
153
185
  },
154
- "default": "'regular'",
155
- "description": "Display variant. `regular` shows only the icon; `extended` adds the label.",
156
- "attribute": "variant",
157
- "reflects": true
186
+ "privacy": "private"
158
187
  },
159
188
  {
160
189
  "kind": "field",
161
- "name": "label",
190
+ "name": "position",
162
191
  "type": {
163
- "text": "string"
192
+ "text": "'bottom-end' | 'bottom-start' | 'top-end' | 'top-start'"
164
193
  },
165
- "default": "''",
166
- "description": "Visible label text shown in the `extended` variant and used as `aria-label` for `regular`.",
167
- "attribute": "label"
194
+ "default": "'bottom-end'",
195
+ "description": "Viewport corner where the FAB is anchored. Uses logical inline/block directions.",
196
+ "attribute": "position",
197
+ "reflects": true
168
198
  },
169
199
  {
170
200
  "kind": "field",
171
- "name": "disabled",
201
+ "name": "scrollReveal",
172
202
  "type": {
173
203
  "text": "boolean"
174
204
  },
175
205
  "default": "false",
176
- "description": "Disables the button.",
177
- "attribute": "disabled",
206
+ "description": "Reveal the FAB only after the page scrolls past `--rc-fab-scroll-threshold` (default 300 px). Uses CSS scroll-driven animations; falls back to a passive scroll listener in unsupported browsers.",
207
+ "attribute": "scroll-reveal",
178
208
  "reflects": true
209
+ },
210
+ {
211
+ "kind": "method",
212
+ "name": "_syncScrollFallback",
213
+ "privacy": "private",
214
+ "return": {
215
+ "type": {
216
+ "text": "void"
217
+ }
218
+ }
219
+ },
220
+ {
221
+ "kind": "method",
222
+ "name": "_getThreshold",
223
+ "privacy": "private",
224
+ "return": {
225
+ "type": {
226
+ "text": "number"
227
+ }
228
+ }
179
229
  }
180
230
  ],
181
231
  "attributes": [
182
232
  {
183
- "name": "variant",
184
- "type": {
185
- "text": "'regular' | 'extended'"
186
- },
187
- "default": "'regular'",
188
- "description": "Display variant. `regular` shows only the icon; `extended` adds the label.",
189
- "fieldName": "variant"
190
- },
191
- {
192
- "name": "label",
233
+ "name": "position",
193
234
  "type": {
194
- "text": "string"
235
+ "text": "'bottom-end' | 'bottom-start' | 'top-end' | 'top-start'"
195
236
  },
196
- "default": "''",
197
- "description": "Visible label text shown in the `extended` variant and used as `aria-label` for `regular`.",
198
- "fieldName": "label"
237
+ "default": "'bottom-end'",
238
+ "description": "Viewport corner where the FAB is anchored. Uses logical inline/block directions.",
239
+ "fieldName": "position"
199
240
  },
200
241
  {
201
- "name": "disabled",
242
+ "name": "scroll-reveal",
202
243
  "type": {
203
244
  "text": "boolean"
204
245
  },
205
246
  "default": "false",
206
- "description": "Disables the button.",
207
- "fieldName": "disabled"
247
+ "description": "Reveal the FAB only after the page scrolls past `--rc-fab-scroll-threshold` (default 300 px). Uses CSS scroll-driven animations; falls back to a passive scroll listener in unsupported browsers.",
248
+ "fieldName": "scrollReveal"
208
249
  }
209
250
  ],
210
251
  "superclass": {
@@ -0,0 +1,215 @@
1
+ import { css as b, LitElement as d, html as h } from "lit";
2
+ import { property as n } from "lit/decorators.js";
3
+ import { NativeChildController as v, ScrollObserverController as f, findNearestScrollAncestor as p } from "@rcarls/rc-common";
4
+ const m = b`
5
+ :host {
6
+ position: var(--rc-fab-position, fixed);
7
+ inset-block-end: var(--rc-fab-inset-block, 1.5rem);
8
+ inset-inline-end: var(--rc-fab-inset-inline, 1.5rem);
9
+ z-index: var(--rc-fab-z-index, 10);
10
+ display: inline-flex;
11
+ }
12
+
13
+ :host([hidden]) {
14
+ display: none;
15
+ }
16
+
17
+ :host([position='bottom-start']) {
18
+ inset-inline-end: unset;
19
+ inset-inline-start: var(--rc-fab-inset-inline, 1.5rem);
20
+ }
21
+
22
+ :host([position='top-end']) {
23
+ inset-block-end: unset;
24
+ inset-block-start: var(--rc-fab-inset-block, 1.5rem);
25
+ }
26
+
27
+ :host([position='top-start']) {
28
+ inset-block-end: unset;
29
+ inset-block-start: var(--rc-fab-inset-block, 1.5rem);
30
+ inset-inline-end: unset;
31
+ inset-inline-start: var(--rc-fab-inset-inline, 1.5rem);
32
+ }
33
+
34
+ ::slotted(button) {
35
+ display: inline-flex;
36
+ align-items: center;
37
+ justify-content: center;
38
+ gap: var(--rc-fab-gap, 0.5rem);
39
+
40
+ min-width: var(--rc-fab-size, 3.5rem);
41
+ height: var(--rc-fab-size, 3.5rem);
42
+ padding-block: 0;
43
+ padding-inline: var(--rc-fab-padding-inline, 1rem);
44
+
45
+ background: var(--rc-fab-bg, ButtonFace);
46
+ color: var(--rc-fab-color, ButtonText);
47
+
48
+ border: none;
49
+ border-radius: var(--rc-fab-radius, 9999px);
50
+ box-shadow: var(--rc-fab-shadow, var(--rc-shadow-level2, none));
51
+
52
+ font-family: var(--rc-fab-font-family, var(--rc-font-family, inherit));
53
+ font-size: var(--rc-fab-font-size, var(--rc-font-size, 0.875rem));
54
+ font-weight: var(--rc-fab-font-weight, 500);
55
+ letter-spacing: var(--rc-fab-letter-spacing, 0.00625em);
56
+ white-space: nowrap;
57
+
58
+ cursor: pointer;
59
+ transition:
60
+ background var(--rc-fab-transition-duration, var(--rc-motion-duration, 200ms)) ease,
61
+ box-shadow var(--rc-fab-transition-duration, var(--rc-motion-duration, 200ms)) ease,
62
+ transform var(--rc-fab-transition-duration, var(--rc-motion-duration, 200ms)) ease;
63
+ }
64
+
65
+ :host(:has(button:hover)) ::slotted(button) {
66
+ background: var(--rc-fab-bg-hover, var(--rc-fab-bg, ButtonFace));
67
+ box-shadow: var(--rc-fab-shadow-hover, var(--rc-fab-shadow, var(--rc-shadow-level3, none)));
68
+ }
69
+
70
+ :host(:has(button:active)) ::slotted(button) {
71
+ box-shadow: var(--rc-fab-shadow-active, none);
72
+ transform: scale(0.96);
73
+ }
74
+
75
+ :host(:has(button:focus-visible)) ::slotted(button) {
76
+ outline: var(--rc-fab-focus-ring, var(--rc-focus-ring, 2px solid currentColor));
77
+ outline-offset: var(--rc-fab-focus-ring-offset, var(--rc-focus-ring-offset, 2px));
78
+ }
79
+
80
+ :host(:has(button:disabled)) ::slotted(button) {
81
+ opacity: var(--rc-fab-disabled-opacity, var(--rc-disabled-opacity, 0.38));
82
+ pointer-events: none;
83
+ box-shadow: none;
84
+ }
85
+
86
+ @keyframes rc-fab-scroll-reveal {
87
+ from {
88
+ opacity: 0;
89
+ visibility: hidden;
90
+ }
91
+ 50% {
92
+ visibility: visible;
93
+ }
94
+ to {
95
+ opacity: 1;
96
+ visibility: visible;
97
+ }
98
+ }
99
+
100
+ @supports (animation-timeline: scroll()) {
101
+ :host([scroll-reveal]) {
102
+ /*
103
+ * Override for embedded/demo contexts where root does not scroll:
104
+ * style="--rc-fab-scroll-timeline: scroll(nearest block)"
105
+ */
106
+ --rc-fab-scroll-timeline: scroll(root block);
107
+
108
+ animation-name: rc-fab-scroll-reveal;
109
+ animation-duration: 1ms; /* required; scroll position drives progress, not time */
110
+ animation-timing-function: linear;
111
+ animation-fill-mode: both;
112
+ animation-timeline: var(--rc-fab-scroll-timeline);
113
+ animation-range: calc(var(--rc-fab-scroll-threshold, 300px) - 100px)
114
+ var(--rc-fab-scroll-threshold, 300px);
115
+ }
116
+
117
+ /* Keyboard escape hatch: always visible when the button has focus */
118
+ :host([scroll-reveal]):has(button:focus-visible) {
119
+ animation: none;
120
+ opacity: 1;
121
+ visibility: visible;
122
+ }
123
+
124
+ @media (prefers-reduced-motion: reduce) {
125
+ :host([scroll-reveal]) {
126
+ animation-range: var(--rc-fab-scroll-threshold, 300px) var(--rc-fab-scroll-threshold, 300px);
127
+ }
128
+ }
129
+ }
130
+
131
+ /*
132
+ * JS fallback for browsers without scroll-driven animation support.
133
+ * [scroll-below-threshold] is toggled by ScrollObserverController;
134
+ * transitions replicate the CSS animation's show/hide behavior.
135
+ */
136
+ @supports not (animation-timeline: scroll()) {
137
+ /* visible state: visibility snaps immediately, opacity fades in */
138
+ :host([scroll-reveal]) {
139
+ transition:
140
+ opacity 200ms linear,
141
+ visibility 0s linear;
142
+ }
143
+
144
+ /* hidden state: opacity fades first, visibility snaps off after delay */
145
+ :host([scroll-reveal][scroll-below-threshold]) {
146
+ opacity: 0;
147
+ visibility: hidden;
148
+ transition:
149
+ opacity 200ms linear,
150
+ visibility 0s linear 200ms;
151
+ }
152
+
153
+ /* Keyboard escape hatch */
154
+ :host([scroll-reveal][scroll-below-threshold]):has(button:focus-visible) {
155
+ opacity: 1;
156
+ visibility: visible;
157
+ transition: none;
158
+ }
159
+
160
+ @media (prefers-reduced-motion: reduce) {
161
+ :host([scroll-reveal]),
162
+ :host([scroll-reveal][scroll-below-threshold]) {
163
+ transition: none;
164
+ }
165
+ }
166
+ }
167
+ `;
168
+ var u = Object.defineProperty, c = (a, t, r, y) => {
169
+ for (var e = void 0, i = a.length - 1, l; i >= 0; i--)
170
+ (l = a[i]) && (e = l(t, r, e) || e);
171
+ return e && u(t, r, e), e;
172
+ };
173
+ const s = class s extends d {
174
+ constructor() {
175
+ super(), this.position = "bottom-end", this.scrollReveal = !1, new v(this, {
176
+ selector: ":scope > button",
177
+ observe: !0,
178
+ onMissing: () => {
179
+ }
180
+ });
181
+ }
182
+ updated(t) {
183
+ super.updated(t), (t.has("scrollReveal") || this.scrollReveal && !this._scrollCtrl) && this._syncScrollFallback();
184
+ }
185
+ _syncScrollFallback() {
186
+ if (this._scrollCtrl && (this._scrollCtrl.setOptions({ disabled: !0 }), this.removeController(this._scrollCtrl), this._scrollCtrl = void 0, this.removeAttribute("scroll-below-threshold")), !this.scrollReveal || CSS.supports("animation-timeline: scroll()")) return;
187
+ const t = this._getThreshold();
188
+ this._scrollCtrl = new f(this, {
189
+ target: () => p(this),
190
+ threshold: t,
191
+ onScroll: (r) => {
192
+ this.toggleAttribute("scroll-below-threshold", r < t);
193
+ }
194
+ });
195
+ }
196
+ _getThreshold() {
197
+ const t = getComputedStyle(this).getPropertyValue("--rc-fab-scroll-threshold").trim(), r = parseFloat(t);
198
+ return Number.isFinite(r) ? r : 300;
199
+ }
200
+ render() {
201
+ return h`<slot></slot>`;
202
+ }
203
+ };
204
+ s.styles = m;
205
+ let o = s;
206
+ c([
207
+ n({ type: String, reflect: !0 })
208
+ ], o.prototype, "position");
209
+ c([
210
+ n({ type: Boolean, attribute: "scroll-reveal", reflect: !0 })
211
+ ], o.prototype, "scrollReveal");
212
+ export {
213
+ o as R
214
+ };
215
+ //# sourceMappingURL=rc-fab-CKowGgDZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rc-fab-CKowGgDZ.js","sources":["../src/rc-fab.styles.ts","../src/rc-fab.ts"],"sourcesContent":["import { css } from 'lit';\n\nexport const fabStyles = css`\n :host {\n position: var(--rc-fab-position, fixed);\n inset-block-end: var(--rc-fab-inset-block, 1.5rem);\n inset-inline-end: var(--rc-fab-inset-inline, 1.5rem);\n z-index: var(--rc-fab-z-index, 10);\n display: inline-flex;\n }\n\n :host([hidden]) {\n display: none;\n }\n\n :host([position='bottom-start']) {\n inset-inline-end: unset;\n inset-inline-start: var(--rc-fab-inset-inline, 1.5rem);\n }\n\n :host([position='top-end']) {\n inset-block-end: unset;\n inset-block-start: var(--rc-fab-inset-block, 1.5rem);\n }\n\n :host([position='top-start']) {\n inset-block-end: unset;\n inset-block-start: var(--rc-fab-inset-block, 1.5rem);\n inset-inline-end: unset;\n inset-inline-start: var(--rc-fab-inset-inline, 1.5rem);\n }\n\n ::slotted(button) {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--rc-fab-gap, 0.5rem);\n\n min-width: var(--rc-fab-size, 3.5rem);\n height: var(--rc-fab-size, 3.5rem);\n padding-block: 0;\n padding-inline: var(--rc-fab-padding-inline, 1rem);\n\n background: var(--rc-fab-bg, ButtonFace);\n color: var(--rc-fab-color, ButtonText);\n\n border: none;\n border-radius: var(--rc-fab-radius, 9999px);\n box-shadow: var(--rc-fab-shadow, var(--rc-shadow-level2, none));\n\n font-family: var(--rc-fab-font-family, var(--rc-font-family, inherit));\n font-size: var(--rc-fab-font-size, var(--rc-font-size, 0.875rem));\n font-weight: var(--rc-fab-font-weight, 500);\n letter-spacing: var(--rc-fab-letter-spacing, 0.00625em);\n white-space: nowrap;\n\n cursor: pointer;\n transition:\n background var(--rc-fab-transition-duration, var(--rc-motion-duration, 200ms)) ease,\n box-shadow var(--rc-fab-transition-duration, var(--rc-motion-duration, 200ms)) ease,\n transform var(--rc-fab-transition-duration, var(--rc-motion-duration, 200ms)) ease;\n }\n\n :host(:has(button:hover)) ::slotted(button) {\n background: var(--rc-fab-bg-hover, var(--rc-fab-bg, ButtonFace));\n box-shadow: var(--rc-fab-shadow-hover, var(--rc-fab-shadow, var(--rc-shadow-level3, none)));\n }\n\n :host(:has(button:active)) ::slotted(button) {\n box-shadow: var(--rc-fab-shadow-active, none);\n transform: scale(0.96);\n }\n\n :host(:has(button:focus-visible)) ::slotted(button) {\n outline: var(--rc-fab-focus-ring, var(--rc-focus-ring, 2px solid currentColor));\n outline-offset: var(--rc-fab-focus-ring-offset, var(--rc-focus-ring-offset, 2px));\n }\n\n :host(:has(button:disabled)) ::slotted(button) {\n opacity: var(--rc-fab-disabled-opacity, var(--rc-disabled-opacity, 0.38));\n pointer-events: none;\n box-shadow: none;\n }\n\n @keyframes rc-fab-scroll-reveal {\n from {\n opacity: 0;\n visibility: hidden;\n }\n 50% {\n visibility: visible;\n }\n to {\n opacity: 1;\n visibility: visible;\n }\n }\n\n @supports (animation-timeline: scroll()) {\n :host([scroll-reveal]) {\n /*\n * Override for embedded/demo contexts where root does not scroll:\n * style=\"--rc-fab-scroll-timeline: scroll(nearest block)\"\n */\n --rc-fab-scroll-timeline: scroll(root block);\n\n animation-name: rc-fab-scroll-reveal;\n animation-duration: 1ms; /* required; scroll position drives progress, not time */\n animation-timing-function: linear;\n animation-fill-mode: both;\n animation-timeline: var(--rc-fab-scroll-timeline);\n animation-range: calc(var(--rc-fab-scroll-threshold, 300px) - 100px)\n var(--rc-fab-scroll-threshold, 300px);\n }\n\n /* Keyboard escape hatch: always visible when the button has focus */\n :host([scroll-reveal]):has(button:focus-visible) {\n animation: none;\n opacity: 1;\n visibility: visible;\n }\n\n @media (prefers-reduced-motion: reduce) {\n :host([scroll-reveal]) {\n animation-range: var(--rc-fab-scroll-threshold, 300px) var(--rc-fab-scroll-threshold, 300px);\n }\n }\n }\n\n /*\n * JS fallback for browsers without scroll-driven animation support.\n * [scroll-below-threshold] is toggled by ScrollObserverController;\n * transitions replicate the CSS animation's show/hide behavior.\n */\n @supports not (animation-timeline: scroll()) {\n /* visible state: visibility snaps immediately, opacity fades in */\n :host([scroll-reveal]) {\n transition:\n opacity 200ms linear,\n visibility 0s linear;\n }\n\n /* hidden state: opacity fades first, visibility snaps off after delay */\n :host([scroll-reveal][scroll-below-threshold]) {\n opacity: 0;\n visibility: hidden;\n transition:\n opacity 200ms linear,\n visibility 0s linear 200ms;\n }\n\n /* Keyboard escape hatch */\n :host([scroll-reveal][scroll-below-threshold]):has(button:focus-visible) {\n opacity: 1;\n visibility: visible;\n transition: none;\n }\n\n @media (prefers-reduced-motion: reduce) {\n :host([scroll-reveal]),\n :host([scroll-reveal][scroll-below-threshold]) {\n transition: none;\n }\n }\n }\n`;\n\nexport default fabStyles;\n","import { LitElement, html } from 'lit';\nimport type { PropertyValues } from 'lit';\nimport { property } from 'lit/decorators.js';\n\nimport {\n NativeChildController,\n ScrollObserverController,\n findNearestScrollAncestor,\n warnMissingDirectChild,\n} from '@rcarls/rc-common';\n\nimport fabStyles from './rc-fab.styles.js';\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'rc-fab': RCFab;\n }\n}\n\n/**\n * Sticky floating action button modeled after the Material 3 Floating action button,\n * wrapping a consumer-supplied button with scroll-aware visibility.\n *\n * Adapted from the Material Design FAB component, use this for \"back to top\",\n * sticky CTAs, chat launchers, and of course as FABs in your Material Design PWA.\n *\n * Place a native `<button>` as the direct child. The button's own accessible\n * name (text content or `aria-label`) becomes the FAB's accessible name.\n *\n * Icons go inside the button alongside or instead of visible text.\n *\n * @see {@link https://richardcarls.github.io/rc-webcomponents/components/rc-fab rc-fab docs}\n * @see {@link https://m3.material.io/components/floating-action-button/overview Material 3 Floating action button}\n *\n * @example Back to top (scroll-triggered)\n * ```html\n * <rc-fab scroll-reveal>\n * <button type=\"button\" aria-label=\"Back to top\" onclick=\"scrollTo({top:0,behavior:'smooth'})\">\n * <span aria-hidden=\"true\">↑</span>\n * </button>\n * </rc-fab>\n * ```\n *\n * @example Icon-only FAB (always visible)\n * ```html\n * <rc-fab>\n * <button type=\"button\" aria-label=\"Create\">\n * <span class=\"material-symbols-outlined\" aria-hidden=\"true\">add</span>\n * </button>\n * </rc-fab>\n * ```\n *\n * @example Extended FAB (icon + visible label)\n * ```html\n * <rc-fab>\n * <button type=\"button\">\n * <span class=\"material-symbols-outlined\" aria-hidden=\"true\">edit</span>\n * Compose\n * </button>\n * </rc-fab>\n * ```\n *\n * @slot default - The native `<button>` element. The button's own accessible\n * name (text content or `aria-label`) serves as the FAB's accessible name.\n *\n * @cssprop [--rc-fab-position=fixed] - CSS position value. Override to `absolute` for layout-relative placement or `sticky` for scroll-snapping.\n * @cssprop [--rc-fab-inset-block=1.5rem] - Distance from the block-axis edge.\n * @cssprop [--rc-fab-inset-inline=1.5rem] - Distance from the inline-axis edge.\n * @cssprop [--rc-fab-z-index=10] - Stacking order.\n * @cssprop [--rc-fab-bg=ButtonFace] - Button background colour.\n * @cssprop [--rc-fab-bg-hover=var(--rc-fab-bg)] - Hover background colour.\n * @cssprop [--rc-fab-color=ButtonText] - Button foreground colour.\n * @cssprop [--rc-fab-size=3.5rem] - Height and minimum width.\n * @cssprop [--rc-fab-radius=9999px] - Border-radius. Default is pill-shaped. Override to `50%` for a circle (icon-only), `1rem` for Material rounded-square, etc.\n * @cssprop [--rc-fab-shadow=none] - Elevation shadow.\n * @cssprop [--rc-fab-shadow-hover=var(--rc-fab-shadow)] - Hover shadow.\n * @cssprop [--rc-fab-shadow-active=none] - Pressed shadow.\n * @cssprop [--rc-fab-padding-inline=1rem] - Inline padding.\n * @cssprop [--rc-fab-gap=0.5rem] - Gap between icon and label text.\n * @cssprop [--rc-fab-font-family=inherit] - Font family for label text.\n * @cssprop [--rc-fab-font-size=0.875rem] - Font size for label text.\n * @cssprop [--rc-fab-font-weight=500] - Font weight for label text.\n * @cssprop [--rc-fab-letter-spacing=0.00625em] - Letter spacing for label text.\n * @cssprop [--rc-fab-focus-ring=2px solid currentColor] - Focus ring style.\n * @cssprop [--rc-fab-focus-ring-offset=2px] - Focus ring offset.\n * @cssprop [--rc-fab-disabled-opacity=0.38] - Opacity applied when the button is disabled.\n * @cssprop [--rc-fab-transition-duration=200ms] - Transition speed for hover and active states.\n * @cssprop [--rc-fab-scroll-threshold=300px] - Scroll distance at which the FAB becomes fully visible. Requires the `scroll-reveal` attribute. The JS fallback reads this value once on connect; px units only.\n * @cssprop [--rc-fab-scroll-timeline=scroll(root block)] - The `animation-timeline` value used for scroll-reveal. Override to target a different scroller, e.g. `scroll(nearest block)` for embedded contexts. CSS path only; the JS fallback discovers the nearest scrollable ancestor automatically.\n *\n */\nexport class RCFab extends LitElement {\n static override styles = fabStyles;\n\n private _scrollCtrl?: ScrollObserverController;\n\n /** Viewport corner where the FAB is anchored. Uses logical inline/block directions. */\n @property({ type: String, reflect: true })\n position: 'bottom-end' | 'bottom-start' | 'top-end' | 'top-start' = 'bottom-end';\n\n /** Reveal the FAB only after the page scrolls past `--rc-fab-scroll-threshold` (default 300 px). Uses CSS scroll-driven animations; falls back to a passive scroll listener in unsupported browsers. */\n @property({ type: Boolean, attribute: 'scroll-reveal', reflect: true })\n scrollReveal = false;\n\n constructor() {\n super();\n\n new NativeChildController<HTMLButtonElement>(this, {\n selector: ':scope > button',\n observe: true,\n onMissing: () => {\n if (import.meta.env.DEV) {\n warnMissingDirectChild(this, {\n selector: ':scope > button',\n message:\n '[rc-fab] No direct child <button> found. Place a native <button> inside <rc-fab>.',\n });\n }\n },\n });\n }\n\n protected override updated(changed: PropertyValues): void {\n super.updated(changed);\n\n if (changed.has('scrollReveal') || (this.scrollReveal && !this._scrollCtrl)) {\n this._syncScrollFallback();\n }\n }\n\n private _syncScrollFallback(): void {\n if (this._scrollCtrl) {\n this._scrollCtrl.setOptions({ disabled: true });\n this.removeController(this._scrollCtrl);\n this._scrollCtrl = undefined;\n\n this.removeAttribute('scroll-below-threshold');\n }\n\n if (!this.scrollReveal || CSS.supports('animation-timeline: scroll()')) return;\n\n const threshold = this._getThreshold();\n\n this._scrollCtrl = new ScrollObserverController(this, {\n target: () => findNearestScrollAncestor(this),\n threshold,\n onScroll: (scrollTop) => {\n this.toggleAttribute('scroll-below-threshold', scrollTop < threshold);\n },\n });\n }\n\n private _getThreshold(): number {\n const raw = getComputedStyle(this).getPropertyValue('--rc-fab-scroll-threshold').trim();\n const n = parseFloat(raw);\n\n return Number.isFinite(n) ? n : 300;\n }\n\n protected override render() {\n return html`<slot></slot>`;\n }\n}\n\nexport default RCFab;\n"],"names":["fabStyles","css","_RCFab","LitElement","NativeChildController","changed","threshold","ScrollObserverController","findNearestScrollAncestor","scrollTop","raw","n","html","RCFab","__decorateClass","property"],"mappings":";;;AAEO,MAAMA,IAAYC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;;;;ACyFlB,MAAMC,IAAN,MAAMA,UAAcC,EAAW;AAAA,EAapC,cAAc;AACZ,UAAA,GAPF,KAAA,WAAoE,cAIpE,KAAA,eAAe,IAKb,IAAIC,EAAyC,MAAM;AAAA,MACjD,UAAU;AAAA,MACV,SAAS;AAAA,MACT,WAAW,MAAM;AAAA,MAQjB;AAAA,IAAA,CACD;AAAA,EACH;AAAA,EAEmB,QAAQC,GAA+B;AACxD,UAAM,QAAQA,CAAO,IAEjBA,EAAQ,IAAI,cAAc,KAAM,KAAK,gBAAgB,CAAC,KAAK,gBAC7D,KAAK,oBAAA;AAAA,EAET;AAAA,EAEQ,sBAA4B;AASlC,QARI,KAAK,gBACP,KAAK,YAAY,WAAW,EAAE,UAAU,IAAM,GAC9C,KAAK,iBAAiB,KAAK,WAAW,GACtC,KAAK,cAAc,QAEnB,KAAK,gBAAgB,wBAAwB,IAG3C,CAAC,KAAK,gBAAgB,IAAI,SAAS,8BAA8B,EAAG;AAExE,UAAMC,IAAY,KAAK,cAAA;AAEvB,SAAK,cAAc,IAAIC,EAAyB,MAAM;AAAA,MACpD,QAAQ,MAAMC,EAA0B,IAAI;AAAA,MAC5C,WAAAF;AAAA,MACA,UAAU,CAACG,MAAc;AACvB,aAAK,gBAAgB,0BAA0BA,IAAYH,CAAS;AAAA,MACtE;AAAA,IAAA,CACD;AAAA,EACH;AAAA,EAEQ,gBAAwB;AAC9B,UAAMI,IAAM,iBAAiB,IAAI,EAAE,iBAAiB,2BAA2B,EAAE,KAAA,GAC3EC,IAAI,WAAWD,CAAG;AAExB,WAAO,OAAO,SAASC,CAAC,IAAIA,IAAI;AAAA,EAClC;AAAA,EAEmB,SAAS;AAC1B,WAAOC;AAAA,EACT;AACF;AAtEEV,EAAgB,SAASF;AADpB,IAAMa,IAANX;AAOLY,EAAA;AAAA,EADCC,EAAS,EAAE,MAAM,QAAQ,SAAS,IAAM;AAAA,GAN9BF,EAOX,WAAA,UAAA;AAIAC,EAAA;AAAA,EADCC,EAAS,EAAE,MAAM,SAAS,WAAW,iBAAiB,SAAS,IAAM;AAAA,GAV3DF,EAWX,WAAA,cAAA;"}
@@ -1,4 +1,4 @@
1
- import { R as e } from "./rc-fab-Cd1-mEO-.js";
1
+ import { R as e } from "./rc-fab-CKowGgDZ.js";
2
2
  customElements.get("rc-fab") || customElements.define("rc-fab", e);
3
3
  export {
4
4
  e as RCFab
package/dist/rc-fab.js CHANGED
@@ -1,4 +1,4 @@
1
- import { R as r } from "./rc-fab-Cd1-mEO-.js";
1
+ import { R as r } from "./rc-fab-CKowGgDZ.js";
2
2
  export {
3
3
  r as RCFab
4
4
  };
@@ -1,50 +1,92 @@
1
- import { LitElement } from 'lit';
1
+ import { LitElement, PropertyValues } from 'lit';
2
2
  declare global {
3
3
  interface HTMLElementTagNameMap {
4
4
  'rc-fab': RCFab;
5
5
  }
6
6
  }
7
7
  /**
8
- * A Material Design 3 Floating Action Button.
8
+ * Sticky floating action button modeled after the Material 3 Floating action button,
9
+ * wrapping a consumer-supplied button with scroll-aware visibility.
9
10
  *
10
- * Use `variant="regular"` (default) for an icon-only FAB, or
11
- * `variant="extended"` to show an icon alongside the `label`.
11
+ * Adapted from the Material Design FAB component, use this for "back to top",
12
+ * sticky CTAs, chat launchers, and of course as FABs in your Material Design PWA.
12
13
  *
13
- * Place the button icon in the `icon` slot:
14
+ * Place a native `<button>` as the direct child. The button's own accessible
15
+ * name (text content or `aria-label`) becomes the FAB's accessible name.
16
+ *
17
+ * Icons go inside the button alongside or instead of visible text.
18
+ *
19
+ * @see {@link https://richardcarls.github.io/rc-webcomponents/components/rc-fab rc-fab docs}
20
+ * @see {@link https://m3.material.io/components/floating-action-button/overview Material 3 Floating action button}
21
+ *
22
+ * @example Back to top (scroll-triggered)
23
+ * ```html
24
+ * <rc-fab scroll-reveal>
25
+ * <button type="button" aria-label="Back to top" onclick="scrollTo({top:0,behavior:'smooth'})">
26
+ * <span aria-hidden="true">↑</span>
27
+ * </button>
28
+ * </rc-fab>
29
+ * ```
30
+ *
31
+ * @example Icon-only FAB (always visible)
14
32
  * ```html
15
- * <rc-fab label="New recipe">
16
- * <iconify-icon slot="icon" icon="mdi-plus"></iconify-icon>
33
+ * <rc-fab>
34
+ * <button type="button" aria-label="Create">
35
+ * <span class="material-symbols-outlined" aria-hidden="true">add</span>
36
+ * </button>
17
37
  * </rc-fab>
18
38
  * ```
19
39
  *
20
- * @slot icon - The icon element displayed inside the button.
21
- * @csspart button - The inner `<button>` element.
22
- * @cssprop [--rc-fab-bg=Canvas] - Button background colour.
40
+ * @example Extended FAB (icon + visible label)
41
+ * ```html
42
+ * <rc-fab>
43
+ * <button type="button">
44
+ * <span class="material-symbols-outlined" aria-hidden="true">edit</span>
45
+ * Compose
46
+ * </button>
47
+ * </rc-fab>
48
+ * ```
49
+ *
50
+ * @slot default - The native `<button>` element. The button's own accessible
51
+ * name (text content or `aria-label`) serves as the FAB's accessible name.
52
+ *
53
+ * @cssprop [--rc-fab-position=fixed] - CSS position value. Override to `absolute` for layout-relative placement or `sticky` for scroll-snapping.
54
+ * @cssprop [--rc-fab-inset-block=1.5rem] - Distance from the block-axis edge.
55
+ * @cssprop [--rc-fab-inset-inline=1.5rem] - Distance from the inline-axis edge.
56
+ * @cssprop [--rc-fab-z-index=10] - Stacking order.
57
+ * @cssprop [--rc-fab-bg=ButtonFace] - Button background colour.
23
58
  * @cssprop [--rc-fab-bg-hover=var(--rc-fab-bg)] - Hover background colour.
24
- * @cssprop [--rc-fab-color=CanvasText] - Foreground (icon + label) colour.
25
- * @cssprop [--rc-fab-size=3.5rem] - Height and minimum width of the button.
26
- * @cssprop [--rc-fab-radius=1rem] - Border-radius of the button.
27
- * @cssprop [--rc-fab-shadow] - Box shadow at rest.
28
- * @cssprop [--rc-fab-shadow-hover] - Box shadow on hover.
29
- * @cssprop [--rc-fab-shadow-active] - Box shadow while pressed.
30
- * @cssprop [--rc-fab-padding-inline=1rem] - Inline padding (extended variant).
31
- * @cssprop [--rc-fab-gap=0.5rem] - Gap between icon and label (extended variant).
32
- * @cssprop [--rc-fab-icon-size=1.5rem] - Font-size applied to the slotted icon.
33
- * @cssprop [--rc-fab-font-size=0.875rem] - Label font size (extended variant).
34
- * @cssprop [--rc-fab-font-weight=500] - Label font weight.
35
- * @cssprop [--rc-fab-letter-spacing=0.00625em] - Label letter spacing.
59
+ * @cssprop [--rc-fab-color=ButtonText] - Button foreground colour.
60
+ * @cssprop [--rc-fab-size=3.5rem] - Height and minimum width.
61
+ * @cssprop [--rc-fab-radius=9999px] - Border-radius. Default is pill-shaped. Override to `50%` for a circle (icon-only), `1rem` for Material rounded-square, etc.
62
+ * @cssprop [--rc-fab-shadow=none] - Elevation shadow.
63
+ * @cssprop [--rc-fab-shadow-hover=var(--rc-fab-shadow)] - Hover shadow.
64
+ * @cssprop [--rc-fab-shadow-active=none] - Pressed shadow.
65
+ * @cssprop [--rc-fab-padding-inline=1rem] - Inline padding.
66
+ * @cssprop [--rc-fab-gap=0.5rem] - Gap between icon and label text.
67
+ * @cssprop [--rc-fab-font-family=inherit] - Font family for label text.
68
+ * @cssprop [--rc-fab-font-size=0.875rem] - Font size for label text.
69
+ * @cssprop [--rc-fab-font-weight=500] - Font weight for label text.
70
+ * @cssprop [--rc-fab-letter-spacing=0.00625em] - Letter spacing for label text.
36
71
  * @cssprop [--rc-fab-focus-ring=2px solid currentColor] - Focus ring style.
37
72
  * @cssprop [--rc-fab-focus-ring-offset=2px] - Focus ring offset.
38
- * @cssprop [--rc-fab-transition-duration=200ms] - Transition speed for hover/active.
73
+ * @cssprop [--rc-fab-disabled-opacity=0.38] - Opacity applied when the button is disabled.
74
+ * @cssprop [--rc-fab-transition-duration=200ms] - Transition speed for hover and active states.
75
+ * @cssprop [--rc-fab-scroll-threshold=300px] - Scroll distance at which the FAB becomes fully visible. Requires the `scroll-reveal` attribute. The JS fallback reads this value once on connect; px units only.
76
+ * @cssprop [--rc-fab-scroll-timeline=scroll(root block)] - The `animation-timeline` value used for scroll-reveal. Override to target a different scroller, e.g. `scroll(nearest block)` for embedded contexts. CSS path only; the JS fallback discovers the nearest scrollable ancestor automatically.
77
+ *
39
78
  */
40
79
  export declare class RCFab extends LitElement {
41
80
  static styles: import('lit').CSSResult;
42
- /** Display variant. `regular` shows only the icon; `extended` adds the label. */
43
- variant: 'regular' | 'extended';
44
- /** Visible label text shown in the `extended` variant and used as `aria-label` for `regular`. */
45
- label: string;
46
- /** Disables the button. */
47
- disabled: boolean;
81
+ private _scrollCtrl?;
82
+ /** Viewport corner where the FAB is anchored. Uses logical inline/block directions. */
83
+ position: 'bottom-end' | 'bottom-start' | 'top-end' | 'top-start';
84
+ /** Reveal the FAB only after the page scrolls past `--rc-fab-scroll-threshold` (default 300 px). Uses CSS scroll-driven animations; falls back to a passive scroll listener in unsupported browsers. */
85
+ scrollReveal: boolean;
86
+ constructor();
87
+ protected updated(changed: PropertyValues): void;
88
+ private _syncScrollFallback;
89
+ private _getThreshold;
48
90
  protected render(): import('lit').TemplateResult<1>;
49
91
  }
50
92
  export default RCFab;
package/package.json CHANGED
@@ -3,14 +3,14 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.2.0",
7
- "description": "Headless floating action button web component built with Lit",
6
+ "version": "0.3.0",
7
+ "description": "Sticky floating action button modeled after the Material 3 Floating action button, wrapping a consumer-supplied button with scroll-aware visibility.",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/richardcarls/rc-webcomponents.git",
11
11
  "directory": "packages/rc-fab"
12
12
  },
13
- "homepage": "https://github.com/richardcarls/rc-webcomponents#readme",
13
+ "homepage": "https://richardcarls.github.io/rc-webcomponents/components/rc-fab",
14
14
  "license": "MIT",
15
15
  "type": "module",
16
16
  "files": [
@@ -37,18 +37,18 @@
37
37
  ],
38
38
  "customElements": "dist/custom-elements.json",
39
39
  "scripts": {
40
- "dev": "vite",
41
40
  "build": "tsc && vite build && cem analyze",
42
41
  "cem:analyze": "cem analyze",
43
42
  "preview": "vite preview",
44
- "test:browser": "vitest",
45
- "test:browser:chrome": "vitest --project=chromium",
46
- "test:browser:firefox": "vitest --project=firefox",
47
- "yalc:publish": "yalc publish --push"
43
+ "test:browser": "vitest --run",
44
+ "test:browser:chrome": "vitest --run --project=chromium",
45
+ "test:browser:firefox": "vitest --run --project=firefox"
46
+ },
47
+ "dependencies": {
48
+ "@rcarls/rc-common": "workspace:*"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@custom-elements-manifest/analyzer": "0.11.0",
51
- "@guanghechen/rollup-plugin-copy": "^6.0.9",
52
52
  "@vitest/browser-playwright": "4.1.5",
53
53
  "lit": "^3.0.0",
54
54
  "playwright": "^1.56.0",
package/dist/demo.css DELETED
@@ -1,85 +0,0 @@
1
- *, *::before, *::after {
2
- box-sizing: border-box;
3
- }
4
-
5
- :root {
6
- color-scheme: light dark;
7
- --rc-accent: Highlight; /* Chrome doesn't support AccentColor on the open web */
8
- }
9
-
10
- @supports (color: AccentColor) {
11
- :root {
12
- --rc-accent: AccentColor;
13
- }
14
- }
15
-
16
- [data-theme="light"] { color-scheme: light; }
17
- [data-theme="dark"] { color-scheme: dark; }
18
-
19
- [data-theme="solarized-light"] {
20
- color-scheme: light;
21
- --rc-surface: #fdf6e3;
22
- --rc-text: #657b83;
23
- --rc-border: 1px solid #93a1a1;
24
- --rc-shadow: 0 2px 8px rgba(0, 0, 0, .12);
25
- }
26
-
27
- [data-theme="solarized-dark"] {
28
- color-scheme: dark;
29
- --rc-surface: #002b36;
30
- --rc-text: #839496;
31
- --rc-border: 1px solid #586e75;
32
- --rc-shadow: 0 2px 8px rgba(0, 0, 0, .3);
33
- }
34
-
35
- body {
36
- font-family: system-ui, sans-serif;
37
- margin: 0;
38
- }
39
-
40
- .demo-page {
41
- max-width: 60rem;
42
- margin: 0 auto;
43
- padding: 2rem 1.5rem;
44
- }
45
-
46
- .demo-controls {
47
- display: flex;
48
- align-items: center;
49
- gap: 1rem;
50
- margin-bottom: 2rem;
51
- padding-bottom: 1rem;
52
- border-bottom: 1px solid ButtonBorder;
53
- }
54
-
55
- .demo-controls .demo-title {
56
- flex: 1;
57
- }
58
-
59
- .demo-controls h1 {
60
- margin: 0 0 0.15rem;
61
- font-size: 1.5rem;
62
- }
63
-
64
- .demo-controls a {
65
- font-size: 0.8rem;
66
- }
67
-
68
- .demo-section {
69
- margin-bottom: 2rem;
70
- }
71
-
72
- .demo-section h2 {
73
- margin: 0 0 0.5rem;
74
- }
75
-
76
- .theme-picker {
77
- padding: 0.35em 0.75em;
78
- font-family: inherit;
79
- font-size: 0.8rem;
80
- cursor: pointer;
81
- border: 1px solid ButtonBorder;
82
- border-radius: 4px;
83
- background: Canvas;
84
- color: CanvasText;
85
- }
package/dist/demo.js DELETED
@@ -1,40 +0,0 @@
1
- const THEMES = ['', 'light', 'dark', 'solarized-light', 'solarized-dark'];
2
- const LABELS = ['Auto', 'Light', 'Dark', 'Solarized ☀', 'Solarized ☾'];
3
-
4
- const stored = localStorage.getItem('rc-demo-theme') ?? '';
5
- applyTheme(stored);
6
-
7
- function applyTheme(theme) {
8
- if (theme) {
9
- document.documentElement.dataset.theme = theme;
10
- } else {
11
- delete document.documentElement.dataset.theme;
12
- }
13
- }
14
-
15
- function currentIndex() {
16
- const current = document.documentElement.dataset.theme ?? '';
17
- const idx = THEMES.indexOf(current);
18
- return idx === -1 ? 0 : idx;
19
- }
20
-
21
- function updateButtons() {
22
- const label = LABELS[currentIndex()];
23
- document.querySelectorAll('.theme-picker').forEach((btn) => {
24
- btn.textContent = label;
25
- });
26
- }
27
-
28
- window.cycleTheme = function () {
29
- const next = THEMES[(currentIndex() + 1) % THEMES.length];
30
- applyTheme(next);
31
- localStorage.setItem('rc-demo-theme', next);
32
- updateButtons();
33
- };
34
-
35
- document.addEventListener('DOMContentLoaded', () => {
36
- updateButtons();
37
- document.querySelectorAll('.theme-picker').forEach((btn) => {
38
- btn.addEventListener('click', window.cycleTheme);
39
- });
40
- });
@@ -1,120 +0,0 @@
1
- import { css as d, LitElement as c, html as f } from "lit";
2
- import { property as t } from "lit/decorators.js";
3
- const p = d`
4
- :host {
5
- display: inline-flex;
6
- }
7
-
8
- :host([hidden]) {
9
- display: none;
10
- }
11
-
12
- button {
13
- display: inline-flex;
14
- align-items: center;
15
- justify-content: center;
16
- gap: var(--rc-fab-gap, 0.5rem);
17
-
18
- min-width: var(--rc-fab-size, 3.5rem);
19
- height: var(--rc-fab-size, 3.5rem);
20
- padding-block: 0;
21
- padding-inline: var(--rc-fab-padding-inline, 1rem);
22
-
23
- background: var(--rc-fab-bg, Canvas);
24
- color: var(--rc-fab-color, CanvasText);
25
-
26
- border: none;
27
- border-radius: var(--rc-fab-radius, 1rem);
28
- box-shadow: var(--rc-fab-shadow, 0 3px 8px rgb(0 0 0 / 0.3));
29
-
30
- font-family: var(--rc-fab-font-family, inherit);
31
- font-size: var(--rc-fab-font-size, 0.875rem);
32
- font-weight: var(--rc-fab-font-weight, 500);
33
- letter-spacing: var(--rc-fab-letter-spacing, 0.00625em);
34
- white-space: nowrap;
35
-
36
- cursor: pointer;
37
- transition:
38
- background var(--rc-fab-transition-duration, 200ms) ease,
39
- box-shadow var(--rc-fab-transition-duration, 200ms) ease,
40
- transform var(--rc-fab-transition-duration, 200ms) ease;
41
-
42
- &:hover {
43
- background: var(--rc-fab-bg-hover, var(--rc-fab-bg, Canvas));
44
- box-shadow: var(--rc-fab-shadow-hover, 0 6px 16px rgb(0 0 0 / 0.3));
45
- }
46
-
47
- &:active {
48
- box-shadow: var(--rc-fab-shadow-active, 0 2px 4px rgb(0 0 0 / 0.3));
49
- transform: scale(0.96);
50
- }
51
-
52
- &:focus-visible {
53
- outline: var(--rc-fab-focus-ring, 2px solid currentColor);
54
- outline-offset: var(--rc-fab-focus-ring-offset, 2px);
55
- }
56
-
57
- &:disabled {
58
- opacity: 0.38;
59
- pointer-events: none;
60
- box-shadow: none;
61
- }
62
- }
63
-
64
- /* Regular variant: icon-only, square-ish with equal padding */
65
- :host([variant='regular']) button {
66
- padding-inline: 0;
67
- }
68
-
69
- #label {
70
- display: none;
71
- }
72
-
73
- /* Extended variant: icon + label */
74
- :host([variant='extended']) #label {
75
- display: inline;
76
- }
77
-
78
- slot[name='icon']::slotted(*) {
79
- font-size: var(--rc-fab-icon-size, 1.5rem);
80
- flex-shrink: 0;
81
- }
82
- `;
83
- var v = Object.defineProperty, n = (o, s, l, h) => {
84
- for (var a = void 0, e = o.length - 1, b; e >= 0; e--)
85
- (b = o[e]) && (a = b(s, l, a) || a);
86
- return a && v(s, l, a), a;
87
- };
88
- const i = class i extends c {
89
- constructor() {
90
- super(...arguments), this.variant = "regular", this.label = "", this.disabled = !1;
91
- }
92
- render() {
93
- return f`
94
- <button
95
- part="button"
96
- type="button"
97
- ?disabled=${this.disabled}
98
- aria-label=${this.variant === "regular" ? this.label : ""}
99
- >
100
- <slot name="icon"></slot>
101
- <span id="label">${this.label}</span>
102
- </button>
103
- `;
104
- }
105
- };
106
- i.styles = p;
107
- let r = i;
108
- n([
109
- t({ type: String, reflect: !0 })
110
- ], r.prototype, "variant");
111
- n([
112
- t({ type: String })
113
- ], r.prototype, "label");
114
- n([
115
- t({ type: Boolean, reflect: !0 })
116
- ], r.prototype, "disabled");
117
- export {
118
- r as R
119
- };
120
- //# sourceMappingURL=rc-fab-Cd1-mEO-.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"rc-fab-Cd1-mEO-.js","sources":["../src/rc-fab.styles.ts","../src/rc-fab.ts"],"sourcesContent":["import { css } from 'lit';\n\nexport const fabStyles = css`\n :host {\n display: inline-flex;\n }\n\n :host([hidden]) {\n display: none;\n }\n\n button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--rc-fab-gap, 0.5rem);\n\n min-width: var(--rc-fab-size, 3.5rem);\n height: var(--rc-fab-size, 3.5rem);\n padding-block: 0;\n padding-inline: var(--rc-fab-padding-inline, 1rem);\n\n background: var(--rc-fab-bg, Canvas);\n color: var(--rc-fab-color, CanvasText);\n\n border: none;\n border-radius: var(--rc-fab-radius, 1rem);\n box-shadow: var(--rc-fab-shadow, 0 3px 8px rgb(0 0 0 / 0.3));\n\n font-family: var(--rc-fab-font-family, inherit);\n font-size: var(--rc-fab-font-size, 0.875rem);\n font-weight: var(--rc-fab-font-weight, 500);\n letter-spacing: var(--rc-fab-letter-spacing, 0.00625em);\n white-space: nowrap;\n\n cursor: pointer;\n transition:\n background var(--rc-fab-transition-duration, 200ms) ease,\n box-shadow var(--rc-fab-transition-duration, 200ms) ease,\n transform var(--rc-fab-transition-duration, 200ms) ease;\n\n &:hover {\n background: var(--rc-fab-bg-hover, var(--rc-fab-bg, Canvas));\n box-shadow: var(--rc-fab-shadow-hover, 0 6px 16px rgb(0 0 0 / 0.3));\n }\n\n &:active {\n box-shadow: var(--rc-fab-shadow-active, 0 2px 4px rgb(0 0 0 / 0.3));\n transform: scale(0.96);\n }\n\n &:focus-visible {\n outline: var(--rc-fab-focus-ring, 2px solid currentColor);\n outline-offset: var(--rc-fab-focus-ring-offset, 2px);\n }\n\n &:disabled {\n opacity: 0.38;\n pointer-events: none;\n box-shadow: none;\n }\n }\n\n /* Regular variant: icon-only, square-ish with equal padding */\n :host([variant='regular']) button {\n padding-inline: 0;\n }\n\n #label {\n display: none;\n }\n\n /* Extended variant: icon + label */\n :host([variant='extended']) #label {\n display: inline;\n }\n\n slot[name='icon']::slotted(*) {\n font-size: var(--rc-fab-icon-size, 1.5rem);\n flex-shrink: 0;\n }\n`;\n\nexport default fabStyles;\n","import { LitElement, html } from 'lit';\nimport { property } from 'lit/decorators.js';\n\nimport fabStyles from './rc-fab.styles.js';\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'rc-fab': RCFab;\n }\n}\n\n/**\n * A Material Design 3 Floating Action Button.\n *\n * Use `variant=\"regular\"` (default) for an icon-only FAB, or\n * `variant=\"extended\"` to show an icon alongside the `label`.\n *\n * Place the button icon in the `icon` slot:\n * ```html\n * <rc-fab label=\"New recipe\">\n * <iconify-icon slot=\"icon\" icon=\"mdi-plus\"></iconify-icon>\n * </rc-fab>\n * ```\n *\n * @slot icon - The icon element displayed inside the button.\n * @csspart button - The inner `<button>` element.\n * @cssprop [--rc-fab-bg=Canvas] - Button background colour.\n * @cssprop [--rc-fab-bg-hover=var(--rc-fab-bg)] - Hover background colour.\n * @cssprop [--rc-fab-color=CanvasText] - Foreground (icon + label) colour.\n * @cssprop [--rc-fab-size=3.5rem] - Height and minimum width of the button.\n * @cssprop [--rc-fab-radius=1rem] - Border-radius of the button.\n * @cssprop [--rc-fab-shadow] - Box shadow at rest.\n * @cssprop [--rc-fab-shadow-hover] - Box shadow on hover.\n * @cssprop [--rc-fab-shadow-active] - Box shadow while pressed.\n * @cssprop [--rc-fab-padding-inline=1rem] - Inline padding (extended variant).\n * @cssprop [--rc-fab-gap=0.5rem] - Gap between icon and label (extended variant).\n * @cssprop [--rc-fab-icon-size=1.5rem] - Font-size applied to the slotted icon.\n * @cssprop [--rc-fab-font-size=0.875rem] - Label font size (extended variant).\n * @cssprop [--rc-fab-font-weight=500] - Label font weight.\n * @cssprop [--rc-fab-letter-spacing=0.00625em] - Label letter spacing.\n * @cssprop [--rc-fab-focus-ring=2px solid currentColor] - Focus ring style.\n * @cssprop [--rc-fab-focus-ring-offset=2px] - Focus ring offset.\n * @cssprop [--rc-fab-transition-duration=200ms] - Transition speed for hover/active.\n */\nexport class RCFab extends LitElement {\n static override styles = fabStyles;\n\n /** Display variant. `regular` shows only the icon; `extended` adds the label. */\n @property({ type: String, reflect: true })\n variant: 'regular' | 'extended' = 'regular';\n\n /** Visible label text shown in the `extended` variant and used as `aria-label` for `regular`. */\n @property({ type: String })\n label = '';\n\n /** Disables the button. */\n @property({ type: Boolean, reflect: true })\n disabled = false;\n\n protected override render() {\n return html`\n <button\n part=\"button\"\n type=\"button\"\n ?disabled=${this.disabled}\n aria-label=${this.variant === 'regular' ? this.label : ''}\n >\n <slot name=\"icon\"></slot>\n <span id=\"label\">${this.label}</span>\n </button>\n `;\n }\n}\n\nexport default RCFab;\n"],"names":["fabStyles","css","_RCFab","LitElement","html","RCFab","__decorateClass","property"],"mappings":";;AAEO,MAAMA,IAAYC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;;;;AC0ClB,MAAMC,IAAN,MAAMA,UAAcC,EAAW;AAAA,EAA/B,cAAA;AAAA,UAAA,GAAA,SAAA,GAKL,KAAA,UAAkC,WAIlC,KAAA,QAAQ,IAIR,KAAA,WAAW;AAAA,EAAA;AAAA,EAEQ,SAAS;AAC1B,WAAOC;AAAA;AAAA;AAAA;AAAA,oBAIS,KAAK,QAAQ;AAAA,qBACZ,KAAK,YAAY,YAAY,KAAK,QAAQ,EAAE;AAAA;AAAA;AAAA,2BAGtC,KAAK,KAAK;AAAA;AAAA;AAAA,EAGnC;AACF;AA3BEF,EAAgB,SAASF;AADpB,IAAMK,IAANH;AAKLI,EAAA;AAAA,EADCC,EAAS,EAAE,MAAM,QAAQ,SAAS,IAAM;AAAA,GAJ9BF,EAKX,WAAA,SAAA;AAIAC,EAAA;AAAA,EADCC,EAAS,EAAE,MAAM,OAAA,CAAQ;AAAA,GARfF,EASX,WAAA,OAAA;AAIAC,EAAA;AAAA,EADCC,EAAS,EAAE,MAAM,SAAS,SAAS,IAAM;AAAA,GAZ/BF,EAaX,WAAA,UAAA;"}