nano-skeleton 0.1.1 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - feat: add `text` attribute to force text mode for text wrapped in elements
8
+
9
+ ## 0.2.0
10
+
11
+ ### Minor Changes
12
+
13
+ - feat: add `interactive` attribute to keep loading content focusable
14
+
15
+ - feat: expose `aria-busy` on loading providers
16
+
3
17
  ## 0.1.1
4
18
 
5
19
  ### Patch Changes
package/README.md CHANGED
@@ -34,7 +34,8 @@ for a shimmering placeholder of the same shape.
34
34
  - **Media-aware.** `<img>` and `<video>` keep their box while their content is
35
35
  hidden.
36
36
  - **Accessible by default.** Loading content is made `inert`, so it can't be
37
- focused or clicked, and the shimmer respects `prefers-reduced-motion`.
37
+ focused or clicked, the provider is marked `aria-busy`, and the shimmer
38
+ respects `prefers-reduced-motion`. Opt out per skeleton with `interactive`.
38
39
  - **Light and dark mode** out of the box via `light-dark()`.
39
40
  - **Works everywhere:** plain HTML, any framework, inside Shadow DOM.
40
41
  - **No dependencies**, styles included. No CSS file to import.
@@ -100,7 +101,7 @@ all of its skeletons at once.
100
101
  A `<nano-skeleton>` without a provider simply renders its content as usual.
101
102
 
102
103
  `<nano-skeleton>` works in one of two modes, picked automatically from its
103
- content.
104
+ content (text mode can also be forced with the `text` attribute).
104
105
 
105
106
  ### Text mode
106
107
 
@@ -129,6 +130,34 @@ placeholder text first and replace it with real data later:
129
130
  <h2><nano-skeleton>Loading title…</nano-skeleton></h2>
130
131
  ```
131
132
 
133
+ #### Text wrapped in elements
134
+
135
+ Automatic detection only looks at text placed directly inside the skeleton.
136
+ When the text is wrapped in an inline element, like a link or `<strong>`, add
137
+ `text` to force text mode:
138
+
139
+ ```html
140
+ <p>
141
+ <nano-skeleton text>
142
+ <a href="/posts/42">Read the full post</a>
143
+ </nano-skeleton>
144
+ </p>
145
+ ```
146
+
147
+ The placeholder follows the text lines exactly like plain text does. The
148
+ wrapping elements should be inline: a block or `inline-block` child gets a
149
+ single bar for its whole box.
150
+
151
+ Text decorations, such as a link's underline, stay visible while loading, so the
152
+ placeholder still reads as a link. To hide them too, wrap the link's content in
153
+ a `<span>`:
154
+
155
+ ```html
156
+ <nano-skeleton text>
157
+ <a href="/posts/42"><span>Read the full post</span></a>
158
+ </nano-skeleton>
159
+ ```
160
+
132
161
  ### Element mode
133
162
 
134
163
  If the skeleton contains elements, it becomes `display: contents` (it doesn't
@@ -156,6 +185,38 @@ Because the placeholder is just the element's own box, make sure it has a size
156
185
  while loading, e.g. `width`/`height` attributes or `aspect-ratio` on images, or
157
186
  a fixed size on containers.
158
187
 
188
+ The content is hidden with `opacity: 0`, so if an element inside has a
189
+ `transition` on `opacity` (or `transition: all`), it fades in and out when
190
+ `loading` changes instead of switching instantly.
191
+
192
+ ### Interactive skeletons
193
+
194
+ By default, a loading skeleton is `inert`: its content can't be focused, clicked
195
+ or reached by assistive technology. That's usually what you want, but sometimes
196
+ the content is usable before the data arrives, e.g. a link whose `href` is
197
+ already known, or a button that doesn't depend on the data.
198
+
199
+ It matters most when content goes back into the loading state (a background
200
+ refetch, pagination): an `inert` element that had focus loses it, and keyboard
201
+ users are sent back to the top of the page.
202
+
203
+ Add `interactive` to keep the content focusable and clickable while loading:
204
+
205
+ ```html
206
+ <nano-skeleton-provider loading>
207
+ <nano-skeleton interactive>
208
+ <a href="/posts/42" class="card__link">
209
+ <span>Placeholder post title</span>
210
+ </a>
211
+ </nano-skeleton>
212
+ </nano-skeleton-provider>
213
+ ```
214
+
215
+ The placeholder looks the same, but the content stays in the tab order and
216
+ keeps its accessible name. Make sure that name makes sense while loading, as screen reader users
217
+ will hear it. The provider sets `aria-busy="true"` while loading to tell them
218
+ the region is still updating.
219
+
159
220
  ## Examples
160
221
 
161
222
  ### A card
@@ -291,7 +352,13 @@ import type { DetailedHTMLProps, HTMLAttributes } from "react";
291
352
  declare module "react" {
292
353
  namespace JSX {
293
354
  interface IntrinsicElements {
294
- [COMPONENT_NAME]: DetailedHTMLProps<HTMLAttributes<Skeleton>, Skeleton>;
355
+ [COMPONENT_NAME]: DetailedHTMLProps<
356
+ HTMLAttributes<Skeleton>,
357
+ Skeleton
358
+ > & {
359
+ interactive?: boolean;
360
+ text?: boolean;
361
+ };
295
362
  [PROVIDER_NAME]: DetailedHTMLProps<
296
363
  HTMLAttributes<SkeletonProvider>,
297
364
  SkeletonProvider
@@ -316,16 +383,23 @@ Registers `<nano-skeleton-provider>` and `<nano-skeleton>` with
316
383
  | -------------------- | --------- | --------------------------------------------------------------------- |
317
384
  | `loading` | `boolean` | When present/`true`, all descendant skeletons render as placeholders. |
318
385
 
386
+ While loading, the provider is exposed to assistive technology as
387
+ `aria-busy="true"`. An `aria-busy` attribute you set yourself takes precedence.
388
+
319
389
  ### `<nano-skeleton>`
320
390
 
321
- No attributes to set. It exposes these state attributes, which you can use as
322
- styling hooks:
391
+ | Attribute / property | Type | Description |
392
+ | -------------------- | --------- | ------------------------------------------------------------------------------------------------------------- |
393
+ | `interactive` | `boolean` | Keeps the content focusable and clickable while loading. See [Interactive skeletons](#interactive-skeletons). |
394
+ | `text` | `boolean` | Forces text mode, e.g. for text inside a link. See [Text wrapped in elements](#text-wrapped-in-elements). |
395
+
396
+ It also exposes these state attributes, which you can use as styling hooks:
323
397
 
324
- | Attribute | Set when |
325
- | -------------- | --------------------------------------------------------- |
326
- | `data-loading` | The nearest provider is loading. |
327
- | `data-text` | The skeleton directly contains text (text mode). |
328
- | `inert` | The nearest provider is loading. Blocks focus and clicks. |
398
+ | Attribute | Set when |
399
+ | -------------- | ------------------------------------------------------------------------------------- |
400
+ | `data-loading` | The nearest provider is loading. |
401
+ | `data-text` | The skeleton directly contains text, or `text` is set (text mode). |
402
+ | `inert` | The nearest provider is loading and `interactive` isn't set. Blocks focus and clicks. |
329
403
 
330
404
  ### Exports
331
405
 
@@ -1,6 +1,14 @@
1
1
  //#region src/Skeleton.d.ts
2
2
  export declare class Skeleton extends HTMLElement {
3
3
  #private;
4
+ static observedAttributes: string[];
5
+ /** Keeps the content focusable and clickable while loading */
6
+ get interactive(): boolean;
7
+ set interactive(value: boolean);
8
+ /** Forces text mode, even when the text is wrapped in elements */
9
+ get text(): boolean;
10
+ set text(value: boolean);
11
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
4
12
  connectedCallback(): void;
5
13
  disconnectedCallback(): void;
6
14
  }
package/dist/Skeleton.js CHANGED
@@ -1,2 +1,2 @@
1
- import{SKELETON_CONTEXT as e}from"./constants.js";import{ContextRequestEvent as t}from"./ContextRequestEvent.js";import{adoptStyles as n}from"./styles.js";function r(e){return`adoptedStyleSheets`in e}var i=class extends HTMLElement{#e;#t=new MutationObserver(()=>this.#n());connectedCallback(){let i=this.getRootNode();r(i)&&n(i),this.#n(),this.#r(!1),this.dispatchEvent(new t(e,(e,t)=>{t&&(this.#e=t),this.#r(e)},!0)),this.#t.observe(this,{childList:!0,characterData:!0,subtree:!0})}disconnectedCallback(){this.#e?.(),this.#e=void 0,this.#t.disconnect()}#n(){let e=!1;for(let t of this.childNodes)if(t instanceof Text&&t.data.trim()!==``){e=!0;break}this.toggleAttribute(`data-text`,e)}#r(e){this.toggleAttribute(`inert`,e),this.toggleAttribute(`data-loading`,e)}};export{i as Skeleton};
1
+ import{SKELETON_CONTEXT as e}from"./constants.js";import{ContextRequestEvent as t}from"./ContextRequestEvent.js";import{adoptStyles as n}from"./styles.js";function r(e){return`adoptedStyleSheets`in e}var i=class extends HTMLElement{static observedAttributes=[`interactive`,`text`];#e=!1;#t;#n=new MutationObserver(()=>this.#r());get interactive(){return this.hasAttribute(`interactive`)}set interactive(e){this.toggleAttribute(`interactive`,!!e)}get text(){return this.hasAttribute(`text`)}set text(e){this.toggleAttribute(`text`,!!e)}attributeChangedCallback(e,t,n){t!==n&&(e===`text`?this.#r():this.#i(this.#e))}connectedCallback(){let i=this.getRootNode();r(i)&&n(i),this.#r(),this.#i(!1),this.dispatchEvent(new t(e,(e,t)=>{t&&(this.#t=t),this.#i(e)},!0)),this.#n.observe(this,{childList:!0,characterData:!0,subtree:!0})}disconnectedCallback(){this.#t?.(),this.#t=void 0,this.#n.disconnect()}#r(){let e=this.text;for(let t of this.childNodes){if(e)break;t instanceof Text&&t.data.trim()!==``&&(e=!0)}this.toggleAttribute(`data-text`,e)}#i(e){this.#e=e,this.toggleAttribute(`inert`,e&&!this.interactive),this.toggleAttribute(`data-loading`,e)}};export{i as Skeleton};
2
2
  //# sourceMappingURL=Skeleton.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Skeleton.js","names":["isDocumentOrShadowRoot","node","Skeleton","#update","root","adoptStyles","#render","ContextRequestEvent","SKELETON_CONTEXT","unsubscribe","#unsubscribe","loading","#observer","isText"],"sources":["../src/Skeleton.ts"],"sourcesContent":["import { SKELETON_CONTEXT } from \"#root/constants.js\";\nimport { ContextRequestEvent } from \"#root/ContextRequestEvent.js\";\nimport { adoptStyles } from \"#root/styles.js\";\nimport { type Unsubscribe } from \"#root/types.js\";\n\nfunction isDocumentOrShadowRoot(\n node: Node | Document | ShadowRoot,\n): node is Document | ShadowRoot {\n return \"adoptedStyleSheets\" in node;\n}\n\nexport class Skeleton extends HTMLElement {\n #unsubscribe?: Unsubscribe | undefined;\n #observer = new MutationObserver(() => this.#update());\n\n connectedCallback() {\n const root = this.getRootNode();\n\n if (isDocumentOrShadowRoot(root)) {\n adoptStyles(root);\n }\n\n this.#update();\n this.#render(false);\n\n this.dispatchEvent(\n new ContextRequestEvent(\n SKELETON_CONTEXT,\n (loading, unsubscribe) => {\n if (unsubscribe) this.#unsubscribe = unsubscribe;\n this.#render(loading);\n },\n true,\n ),\n );\n\n this.#observer.observe(this, {\n childList: true,\n characterData: true,\n subtree: true,\n });\n }\n\n disconnectedCallback() {\n this.#unsubscribe?.();\n this.#unsubscribe = undefined;\n this.#observer.disconnect();\n }\n\n #update() {\n let isText = false;\n for (const node of this.childNodes) {\n if (node instanceof Text && node.data.trim() !== \"\") {\n isText = true;\n break;\n }\n }\n this.toggleAttribute(\"data-text\", isText);\n }\n\n #render(loading: boolean) {\n this.toggleAttribute(\"inert\", loading);\n this.toggleAttribute(\"data-loading\", loading);\n }\n}\n"],"mappings":"A,2JAKA,SAASA,EACP,EAC+B,CAC/B,MAAO,uBAAwBC,CACjC,CAEA,IAAaC,EAAb,cAA8B,WAAY,CACxC,GACA,GAAY,IAAI,qBAAuB,KAAKC,GAAQ,CAAC,EAErD,mBAAoB,CAClB,IAAMC,EAAO,KAAK,YAAY,EAE1BJ,EAAuBI,CAAI,GAC7BC,EAAYD,CAAI,EAGlB,KAAKD,GAAQ,EACb,KAAKG,GAAQ,EAAK,EAElB,KAAK,cACH,IAAIC,EACFC,GACC,EAAS,IAAgB,CACpBC,IAAa,KAAKC,GAAeD,GACrC,KAAKH,GAAQK,CAAO,CACtB,EACA,EACF,CACF,EAEA,KAAKC,GAAU,QAAQ,KAAM,CAC3B,UAAW,GACX,cAAe,GACf,QAAS,EACX,CAAC,CACH,CAEA,sBAAuB,CACrB,KAAKF,KAAe,EACpB,KAAKA,GAAe,IAAA,GACpB,KAAKE,GAAU,WAAW,CAC5B,CAEA,IAAU,CACR,IAAIC,EAAS,GACb,IAAK,IAAMZ,KAAQ,KAAK,WACtB,GAAIA,aAAgB,MAAQA,EAAK,KAAK,KAAK,IAAM,GAAI,CACnD,EAAS,GACT,KACF,CAEF,KAAK,gBAAgB,YAAaY,CAAM,CAC1C,CAEA,GAAQ,EAAkB,CACxB,KAAK,gBAAgB,QAASF,CAAO,EACrC,KAAK,gBAAgB,eAAgBA,CAAO,CAC9C,CACF,E"}
1
+ {"version":3,"file":"Skeleton.js","names":["isDocumentOrShadowRoot","node","Skeleton","#update","value","oldValue","newValue","name","#render","#loading","root","adoptStyles","ContextRequestEvent","SKELETON_CONTEXT","unsubscribe","#unsubscribe","loading","#observer","isText"],"sources":["../src/Skeleton.ts"],"sourcesContent":["import { SKELETON_CONTEXT } from \"#root/constants.js\";\nimport { ContextRequestEvent } from \"#root/ContextRequestEvent.js\";\nimport { adoptStyles } from \"#root/styles.js\";\nimport { type Unsubscribe } from \"#root/types.js\";\n\nfunction isDocumentOrShadowRoot(\n node: Node | Document | ShadowRoot,\n): node is Document | ShadowRoot {\n return \"adoptedStyleSheets\" in node;\n}\n\nexport class Skeleton extends HTMLElement {\n static observedAttributes = [\"interactive\", \"text\"];\n\n #loading = false;\n #unsubscribe?: Unsubscribe | undefined;\n #observer = new MutationObserver(() => this.#update());\n\n /** Keeps the content focusable and clickable while loading */\n get interactive() {\n return this.hasAttribute(\"interactive\");\n }\n\n set interactive(value: boolean) {\n this.toggleAttribute(\"interactive\", Boolean(value));\n }\n\n /** Forces text mode, even when the text is wrapped in elements */\n get text() {\n return this.hasAttribute(\"text\");\n }\n\n set text(value: boolean) {\n this.toggleAttribute(\"text\", Boolean(value));\n }\n\n attributeChangedCallback(\n name: string,\n oldValue: string | null,\n newValue: string | null,\n ) {\n if (oldValue === newValue) return;\n if (name === \"text\") this.#update();\n else this.#render(this.#loading);\n }\n\n connectedCallback() {\n const root = this.getRootNode();\n\n if (isDocumentOrShadowRoot(root)) {\n adoptStyles(root);\n }\n\n this.#update();\n this.#render(false);\n\n this.dispatchEvent(\n new ContextRequestEvent(\n SKELETON_CONTEXT,\n (loading, unsubscribe) => {\n if (unsubscribe) this.#unsubscribe = unsubscribe;\n this.#render(loading);\n },\n true,\n ),\n );\n\n this.#observer.observe(this, {\n childList: true,\n characterData: true,\n subtree: true,\n });\n }\n\n disconnectedCallback() {\n this.#unsubscribe?.();\n this.#unsubscribe = undefined;\n this.#observer.disconnect();\n }\n\n #update() {\n let isText = this.text;\n for (const node of this.childNodes) {\n if (isText) break;\n if (node instanceof Text && node.data.trim() !== \"\") {\n isText = true;\n }\n }\n this.toggleAttribute(\"data-text\", isText);\n }\n\n #render(loading: boolean) {\n this.#loading = loading;\n this.toggleAttribute(\"inert\", loading && !this.interactive);\n this.toggleAttribute(\"data-loading\", loading);\n }\n}\n"],"mappings":"A,2JAKA,SAASA,EACP,EAC+B,CAC/B,MAAO,uBAAwBC,CACjC,CAEA,IAAaC,EAAb,cAA8B,WAAY,CACxC,OAAO,mBAAqB,CAAC,cAAe,MAAM,EAElD,GAAW,GACX,GACA,GAAY,IAAI,qBAAuB,KAAKC,GAAQ,CAAC,EAGrD,IAAI,aAAc,CAChB,OAAO,KAAK,aAAa,aAAa,CACxC,CAEA,IAAI,YAAY,EAAgB,CAC9B,KAAK,gBAAgB,cAAe,EAAQC,CAAM,CACpD,CAGA,IAAI,MAAO,CACT,OAAO,KAAK,aAAa,MAAM,CACjC,CAEA,IAAI,KAAK,EAAgB,CACvB,KAAK,gBAAgB,OAAQ,EAAQA,CAAM,CAC7C,CAEA,yBACE,EACA,EACA,EACA,CACIC,IAAaC,IACbC,IAAS,OAAQ,KAAKJ,GAAQ,EAC7B,KAAKK,GAAQ,KAAKC,EAAQ,EACjC,CAEA,mBAAoB,CAClB,IAAMC,EAAO,KAAK,YAAY,EAE1BV,EAAuBU,CAAI,GAC7BC,EAAYD,CAAI,EAGlB,KAAKP,GAAQ,EACb,KAAKK,GAAQ,EAAK,EAElB,KAAK,cACH,IAAII,EACFC,GACC,EAAS,IAAgB,CACpBC,IAAa,KAAKC,GAAeD,GACrC,KAAKN,GAAQQ,CAAO,CACtB,EACA,EACF,CACF,EAEA,KAAKC,GAAU,QAAQ,KAAM,CAC3B,UAAW,GACX,cAAe,GACf,QAAS,EACX,CAAC,CACH,CAEA,sBAAuB,CACrB,KAAKF,KAAe,EACpB,KAAKA,GAAe,IAAA,GACpB,KAAKE,GAAU,WAAW,CAC5B,CAEA,IAAU,CACR,IAAIC,EAAS,KAAK,KAClB,IAAK,IAAMjB,KAAQ,KAAK,WAAY,CAClC,GAAIiB,EAAQ,MACRjB,aAAgB,MAAQA,EAAK,KAAK,KAAK,IAAM,KAC/C,EAAS,GAEb,CACA,KAAK,gBAAgB,YAAaiB,CAAM,CAC1C,CAEA,GAAQ,EAAkB,CACxB,KAAKT,GAAWO,EAChB,KAAK,gBAAgB,QAASA,GAAW,CAAC,KAAK,WAAW,EAC1D,KAAK,gBAAgB,eAAgBA,CAAO,CAC9C,CACF,E"}
@@ -1,2 +1,2 @@
1
- import{SKELETON_CONTEXT as e}from"./constants.js";var t=class extends HTMLElement{static observedAttributes=[`loading`];#e=/* @__PURE__ */ new Set;constructor(){super(),this.addEventListener(`context-request`,t=>{if(t.context!==e||typeof t.callback!=`function`)return;t.stopPropagation();let{callback:n}=t;t.subscribe?(this.#e.add(n),n(this.loading,()=>this.#e.delete(n))):n(this.loading)})}get loading(){return this.hasAttribute(`loading`)}set loading(e){this.toggleAttribute(`loading`,!!e)}attributeChangedCallback(e,t,n){if(t!==n)for(let e of this.#e)e(this.loading)}};export{t as SkeletonProvider};
1
+ import{SKELETON_CONTEXT as e}from"./constants.js";var t=class extends HTMLElement{static observedAttributes=[`loading`];#e=/* @__PURE__ */ new Set;#t=this.attachInternals();constructor(){super(),this.addEventListener(`context-request`,t=>{if(t.context!==e||typeof t.callback!=`function`)return;t.stopPropagation();let{callback:n}=t;t.subscribe?(this.#e.add(n),n(this.loading,()=>this.#e.delete(n))):n(this.loading)})}get loading(){return this.hasAttribute(`loading`)}set loading(e){this.toggleAttribute(`loading`,!!e)}attributeChangedCallback(e,t,n){if(t!==n){this.#t.ariaBusy=this.loading?`true`:null;for(let e of this.#e)e(this.loading)}}};export{t as SkeletonProvider};
2
2
  //# sourceMappingURL=SkeletonProvider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SkeletonProvider.js","names":["SkeletonProvider","event","SKELETON_CONTEXT","#subscribers","callback","value","oldValue","newValue"],"sources":["../src/SkeletonProvider.ts"],"sourcesContent":["import type { ContextRequestEvent } from \"#root/ContextRequestEvent.js\";\nimport type { LoadingCallback } from \"#root/types.js\";\n\nimport { SKELETON_CONTEXT } from \"#root/constants.js\";\n\nexport class SkeletonProvider extends HTMLElement {\n static observedAttributes = [\"loading\"];\n\n #subscribers = new Set<LoadingCallback>();\n\n constructor() {\n super();\n /**\n * Adding listener from the constructor so the provider is ready before any\n * descendant connects and asks for context.\n */\n this.addEventListener(\"context-request\", (event: ContextRequestEvent) => {\n if (\n event.context !== SKELETON_CONTEXT ||\n typeof event.callback !== \"function\"\n )\n return;\n\n /** The nearest provider wins */\n event.stopPropagation();\n\n const { callback } = event;\n\n if (event.subscribe) {\n this.#subscribers.add(callback);\n callback(this.loading, () => this.#subscribers.delete(callback));\n } else {\n callback(this.loading);\n }\n });\n }\n\n get loading() {\n return this.hasAttribute(\"loading\");\n }\n\n set loading(value: boolean) {\n this.toggleAttribute(\"loading\", Boolean(value));\n }\n\n attributeChangedCallback(\n _name: string,\n oldValue: string | null,\n newValue: string | null,\n ) {\n if (oldValue === newValue) return;\n for (const callback of this.#subscribers) callback(this.loading);\n }\n}\n"],"mappings":"A,kDAKA,IAAaA,EAAb,cAAsC,WAAY,CAChD,OAAO,mBAAqB,CAAC,SAAS,EAEtC,mBAAe,IAAI,IAEnB,aAAc,CACZ,MAAM,EAKN,KAAK,iBAAiB,kBAAoB,GAA+B,CACvE,GACEC,EAAM,UAAYC,GAClB,OAAOD,EAAM,UAAa,WAE1B,OAGFA,EAAM,gBAAgB,EAEtB,GAAM,CAAE,YAAaA,EAEjBA,EAAM,WACR,KAAKE,GAAa,IAAIC,CAAQ,EAC9BA,EAAS,KAAK,YAAe,KAAKD,GAAa,OAAOC,CAAQ,CAAC,GAE/DA,EAAS,KAAK,OAAO,CAEzB,CAAC,CACH,CAEA,IAAI,SAAU,CACZ,OAAO,KAAK,aAAa,SAAS,CACpC,CAEA,IAAI,QAAQ,EAAgB,CAC1B,KAAK,gBAAgB,UAAW,EAAQC,CAAM,CAChD,CAEA,yBACE,EACA,EACA,EACA,CACA,GAAIC,IAAaC,EACjB,IAAK,IAAMH,KAAY,KAAKD,GAAcC,EAAS,KAAK,OAAO,CACjE,CACF,E"}
1
+ {"version":3,"file":"SkeletonProvider.js","names":["SkeletonProvider","event","SKELETON_CONTEXT","#subscribers","callback","value","oldValue","newValue","#internals"],"sources":["../src/SkeletonProvider.ts"],"sourcesContent":["import type { ContextRequestEvent } from \"#root/ContextRequestEvent.js\";\nimport type { LoadingCallback } from \"#root/types.js\";\n\nimport { SKELETON_CONTEXT } from \"#root/constants.js\";\n\nexport class SkeletonProvider extends HTMLElement {\n static observedAttributes = [\"loading\"];\n\n #subscribers = new Set<LoadingCallback>();\n #internals = this.attachInternals();\n\n constructor() {\n super();\n /**\n * Adding listener from the constructor so the provider is ready before any\n * descendant connects and asks for context.\n */\n this.addEventListener(\"context-request\", (event: ContextRequestEvent) => {\n if (\n event.context !== SKELETON_CONTEXT ||\n typeof event.callback !== \"function\"\n )\n return;\n\n /** The nearest provider wins */\n event.stopPropagation();\n\n const { callback } = event;\n\n if (event.subscribe) {\n this.#subscribers.add(callback);\n callback(this.loading, () => this.#subscribers.delete(callback));\n } else {\n callback(this.loading);\n }\n });\n }\n\n get loading() {\n return this.hasAttribute(\"loading\");\n }\n\n set loading(value: boolean) {\n this.toggleAttribute(\"loading\", Boolean(value));\n }\n\n attributeChangedCallback(\n _name: string,\n oldValue: string | null,\n newValue: string | null,\n ) {\n if (oldValue === newValue) return;\n /**\n * Via internals, so an aria-busy attribute set by the user still takes\n * precedence.\n */\n this.#internals.ariaBusy = this.loading ? \"true\" : null;\n for (const callback of this.#subscribers) callback(this.loading);\n }\n}\n"],"mappings":"A,kDAKA,IAAaA,EAAb,cAAsC,WAAY,CAChD,OAAO,mBAAqB,CAAC,SAAS,EAEtC,mBAAe,IAAI,IACnB,GAAa,KAAK,gBAAgB,EAElC,aAAc,CACZ,MAAM,EAKN,KAAK,iBAAiB,kBAAoB,GAA+B,CACvE,GACEC,EAAM,UAAYC,GAClB,OAAOD,EAAM,UAAa,WAE1B,OAGFA,EAAM,gBAAgB,EAEtB,GAAM,CAAE,YAAaA,EAEjBA,EAAM,WACR,KAAKE,GAAa,IAAIC,CAAQ,EAC9BA,EAAS,KAAK,YAAe,KAAKD,GAAa,OAAOC,CAAQ,CAAC,GAE/DA,EAAS,KAAK,OAAO,CAEzB,CAAC,CACH,CAEA,IAAI,SAAU,CACZ,OAAO,KAAK,aAAa,SAAS,CACpC,CAEA,IAAI,QAAQ,EAAgB,CAC1B,KAAK,gBAAgB,UAAW,EAAQC,CAAM,CAChD,CAEA,yBACE,EACA,EACA,EACA,CACA,GAAIC,IAAaC,EAKjB,MAAKC,GAAW,SAAW,KAAK,QAAU,OAAS,KACnD,IAAK,IAAMJ,KAAY,KAAKD,GAAcC,EAAS,KAAK,OAAO,CADZ,CAErD,CACF,E"}
package/dist/constants.js CHANGED
@@ -21,10 +21,11 @@ const e=`nano-skeleton`,t=`${e}-provider`,{default:n}={default:`:scope {
21
21
 
22
22
  /*
23
23
  * Everything inside the root should disappear, but nothing jumps,
24
- * as layout is intact.
24
+ * as layout is intact. Hidden visually only: focus and accessibility are
25
+ * handled by inert, so interactive skeletons keep both.
25
26
  */
26
- :scope[data-loading] > * :is(*, *:before, *:after) {
27
- visibility: hidden !important;
27
+ :scope[data-loading] > * > * {
28
+ opacity: 0 !important;
28
29
  }
29
30
 
30
31
  /**
@@ -37,13 +38,26 @@ const e=`nano-skeleton`,t=`${e}-provider`,{default:n}={default:`:scope {
37
38
  var(--nano-skeleton-text-border-radius-default)
38
39
  );
39
40
  box-decoration-break: clone;
40
- text-box-edge: cap alphabetic;
41
- text-box-trim: trim-both;
41
+ text-box: cap alphabetic;
42
42
  /**
43
43
  * This ensures the ellipsis on truncated text is also hidden.
44
44
  * -webkit-text-fill-color overrides any "color" property that is also set.
45
45
  */
46
46
  -webkit-text-fill-color: transparent;
47
+ /**
48
+ * Text might get visible when selected
49
+ */
50
+ user-select: none;
51
+ }
52
+
53
+ /**
54
+ * Children and grandchildren need to inherit these styles
55
+ * for consistent outlines
56
+ */
57
+ :scope[data-loading][data-text] > *,
58
+ :scope[data-loading][data-text] > * > * {
59
+ border-radius: inherit;
60
+ text-box: inherit;
47
61
  }
48
62
 
49
63
  :scope[data-loading] > :is(video, img):only-child {
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","names":["COMPONENT_NAME","PROVIDER_NAME","CSS_CONTENT","SKELETON_CONTEXT"],"sources":["../src/constants.ts"],"sourcesContent":["const isDev = import.meta.env.MODE !== \"production\";\n\nexport const COMPONENT_NAME = \"nano-skeleton\" as const;\n\nexport const PROVIDER_NAME = `${COMPONENT_NAME}-provider` as const;\n\nconst { default: CSS_CONTENT } = isDev\n ? await import(\"./styles.css?inline\")\n : { default: `__CSS_CONTENT__` };\n\nexport { CSS_CONTENT };\n\nexport const SKELETON_CONTEXT = Symbol.for(`${COMPONENT_NAME}-context`);\n"],"mappings":"AAEA,MAAaA,EAAiB,gBAEjBC,EAAgB,GAAGD,EAAe,WAEzC,CAAE,QAASE,GAEb,CAAE,QAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkFb,EAIWC,EAAmB,OAAO,IAAI,GAAGH,EAAe,SAAS,E"}
1
+ {"version":3,"file":"constants.js","names":["COMPONENT_NAME","PROVIDER_NAME","CSS_CONTENT","SKELETON_CONTEXT"],"sources":["../src/constants.ts"],"sourcesContent":["const isDev = import.meta.env.MODE !== \"production\";\n\nexport const COMPONENT_NAME = \"nano-skeleton\" as const;\n\nexport const PROVIDER_NAME = `${COMPONENT_NAME}-provider` as const;\n\nconst { default: CSS_CONTENT } = isDev\n ? await import(\"./styles.css?inline\")\n : { default: `__CSS_CONTENT__` };\n\nexport { CSS_CONTENT };\n\nexport const SKELETON_CONTEXT = Symbol.for(`${COMPONENT_NAME}-context`);\n"],"mappings":"AAEA,MAAaA,EAAiB,gBAEjBC,EAAgB,GAAGD,EAAe,WAEzC,CAAE,QAASE,GAEb,CAAE,QAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgGb,EAIWC,EAAmB,OAAO,IAAI,GAAGH,EAAe,SAAS,E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nano-skeleton",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "The perfect skeleton component",
5
5
  "homepage": "https://github.com/smashercosmo/nano-skeleton#readme",
6
6
  "bugs": {