|
1 |
| -import {GecutDirective} from '@gecut/lit-helper/directives/directive.js'; |
2 |
| -import {noChange, nothing, html} from 'lit/html.js'; |
3 |
| -import {type PartInfo, directive} from 'lit/directive.js'; |
4 |
| -import {gecutButton, type ButtonContent} from '../button/button.js'; |
5 |
| -import {gecutIconButton, type IconButtonContent} from '../components.js'; |
6 |
| -import {classMap, type ClassInfo} from 'lit/directives/class-map.js'; |
| 1 | +import {GecutAsyncDirective} from '@gecut/lit-helper/directives/async-directive.js'; |
| 2 | +import {directive} from 'lit/directive.js'; |
| 3 | +import {classMap} from 'lit/directives/class-map.js'; |
| 4 | +import {nothing, html, noChange} from 'lit/html.js'; |
| 5 | + |
| 6 | +import {gecutButton} from '../button/button.js'; |
| 7 | +import {gecutIconButton} from '../components.js'; |
| 8 | + |
| 9 | +import type {ButtonContent} from '../button/button.js'; |
| 10 | +import type {IconButtonContent} from '../components.js'; |
| 11 | +import type {ContextSignal} from '@gecut/signal'; |
| 12 | +import type {PartInfo} from 'lit/directive.js'; |
| 13 | +import type {ClassInfo} from 'lit/directives/class-map.js'; |
7 | 14 |
|
8 | 15 | export interface SnackBarContent {
|
9 | 16 | message: string;
|
10 | 17 |
|
| 18 | + open?: boolean; |
11 | 19 | action?: Omit<ButtonContent, 'type'>;
|
12 | 20 | close?: boolean | Omit<IconButtonContent, 'type'>;
|
13 | 21 | }
|
14 | 22 |
|
15 |
| -export class GecutSnackBarDirective extends GecutDirective { |
| 23 | +export class GecutSnackBarDirective extends GecutAsyncDirective { |
16 | 24 | constructor(partInfo: PartInfo) {
|
17 | 25 | super(partInfo, 'gecut-snack-bar');
|
18 | 26 | }
|
19 | 27 |
|
20 |
| - protected content?: SnackBarContent; |
| 28 | + protected _$signalContext?: ContextSignal<SnackBarContent>; |
| 29 | + protected _$unsubscribe?: () => void; |
| 30 | + |
| 31 | + render(signalContext: ContextSignal<SnackBarContent>): unknown { |
| 32 | + this.log.methodArgs?.('render', signalContext); |
| 33 | + |
| 34 | + if (this._$signalContext !== signalContext) { |
| 35 | + // When the observable changes, unsubscribe to the old one and subscribe to the new one |
| 36 | + this._$unsubscribe?.(); |
| 37 | + this._$signalContext = signalContext; |
| 38 | + |
| 39 | + if (this.isConnected) { |
| 40 | + this.subscribe(); |
| 41 | + } |
| 42 | + } |
| 43 | + |
| 44 | + return noChange; |
| 45 | + } |
| 46 | + |
| 47 | + // When the directive is disconnected from the DOM, unsubscribe to ensure |
| 48 | + // the directive instance can be garbage collected |
| 49 | + override disconnected(): void { |
| 50 | + super.disconnected(); |
21 | 51 |
|
22 |
| - render(content?: SnackBarContent): unknown { |
23 |
| - this.log.methodArgs?.('render', content); |
| 52 | + this._$unsubscribe!(); |
| 53 | + } |
| 54 | + // If the subtree the directive is in was disconnected and subsequently |
| 55 | + // re-connected, re-subscribe to make the directive operable again |
| 56 | + override reconnected(): void { |
| 57 | + super.reconnected(); |
24 | 58 |
|
25 |
| - if (content === undefined) return noChange; |
| 59 | + this.subscribe(); |
| 60 | + } |
26 | 61 |
|
27 |
| - this.content = content; |
| 62 | + close() { |
| 63 | + if (this._$signalContext?.value?.open) { |
| 64 | + this._$signalContext.value.open = false; |
28 | 65 |
|
29 |
| - return this.renderSnackBar(); |
| 66 | + this._$signalContext?.renotify(); |
| 67 | + } |
30 | 68 | }
|
31 | 69 |
|
32 |
| - protected renderSnackBar() { |
33 |
| - if (!this.content) return nothing; |
| 70 | + protected subscribe() { |
| 71 | + this.log.method?.('subscribe'); |
| 72 | + |
| 73 | + this._$unsubscribe = this._$signalContext?.subscribe( |
| 74 | + (content) => { |
| 75 | + this.setValue(this.renderSnackBar(content)); |
| 76 | + }, |
| 77 | + {receivePrevious: true}, |
| 78 | + ).unsubscribe; |
| 79 | + } |
34 | 80 |
|
| 81 | + protected renderSnackBar(content: SnackBarContent) { |
35 | 82 | this.log.method?.('renderSnackBar');
|
36 | 83 |
|
37 | 84 | return html`
|
38 | 85 | <div class=${classMap(this.getRenderClasses())}>
|
39 |
| - <span class="gecut-snack-bar-message">${this.content.message}</span> |
40 |
| - ${this.renderAction()} ${this.renderClose()} |
| 86 | + <span class="gecut-snack-bar-message">${content.message}</span> |
| 87 | + <div @click=${this.close.bind(this)}> |
| 88 | + ${this.renderAction(content.action)} ${this.renderClose(content.close)} |
| 89 | + </div> |
41 | 90 | </div>
|
42 | 91 | `;
|
43 | 92 | }
|
44 |
| - protected renderAction(): unknown { |
45 |
| - if (!this.content?.action) return nothing; |
| 93 | + protected renderAction(content: SnackBarContent['action']): unknown { |
| 94 | + if (!content) return nothing; |
46 | 95 |
|
47 | 96 | this.log.method?.('renderAction');
|
48 | 97 |
|
49 |
| - return gecutButton({...this.content.action, type: 'text'}); |
| 98 | + return gecutButton({ |
| 99 | + ...content, |
| 100 | + type: 'text', |
| 101 | + }); |
50 | 102 | }
|
51 |
| - protected renderClose(): unknown { |
52 |
| - if (!this.content?.close) return nothing; |
| 103 | + protected renderClose(content: SnackBarContent['close']): unknown { |
| 104 | + if (!content) return nothing; |
53 | 105 |
|
54 | 106 | this.log.method?.('renderClose');
|
55 | 107 |
|
56 |
| - return gecutIconButton({ |
57 |
| - svg: '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-dasharray="12" stroke-dashoffset="12" stroke-linecap="round" stroke-width="2" d="M12 12L19 19M12 12L5 5M12 12L5 19M12 12L19 5"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.2s" values="12;0"/></path></svg>', |
| 108 | + const _content: Omit<IconButtonContent, 'type'> = |
| 109 | + typeof content !== 'boolean' |
| 110 | + ? content |
| 111 | + : { |
| 112 | + svg: |
| 113 | + // eslint-disable-next-line max-len |
| 114 | + '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-dasharray="12" stroke-dashoffset="12" stroke-linecap="round" stroke-width="2" d="M12 12L19 19M12 12L5 5M12 12L5 19M12 12L19 5"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.2s" values="12;0"/></path></svg>', |
| 115 | + }; |
58 | 116 |
|
59 |
| - ...(typeof this.content.close !== 'boolean' ? this.content.close : {}), |
60 |
| - }); |
| 117 | + return gecutIconButton(_content); |
61 | 118 | }
|
62 | 119 |
|
63 | 120 | protected override getRenderClasses(): ClassInfo {
|
| 121 | + const content = this._$signalContext?.value; |
| 122 | + |
| 123 | + if (!content) return super.getRenderClasses(); |
| 124 | + |
64 | 125 | return {
|
65 | 126 | ...super.getRenderClasses(),
|
66 | 127 |
|
67 |
| - 'longer-action': (this.content?.action?.label?.length ?? 0) > 10, |
| 128 | + 'longer-action': (content.action?.label?.length ?? 0) > 10, |
| 129 | + open: content.open ?? false, |
| 130 | + close: !(content.open ?? false), |
68 | 131 | };
|
69 | 132 | }
|
70 | 133 | }
|
|
0 commit comments