@kubex/zinc 1.1.29 → 1.1.31

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.
@@ -132,6 +132,41 @@ for example by adding item-selector="zn-inline-edit" only the inline edit elemen
132
132
  </div>
133
133
  ```
134
134
 
135
- ### Second Example
135
+ ### Pull Tab
136
136
 
137
- TODO
137
+ Add the `pull-tab` attribute to replace the settings dropdown with a pull tab anchored to the top right
138
+ of the container. Clicking the tab slides down a panel containing the settings options. Optional content
139
+ can be placed in the `tab` slot to render alongside the chevron.
140
+
141
+ ```html:preview
142
+
143
+ <div style="width: 100%; height: 300px; border: 1px solid #ccc;">
144
+ <zn-settings-container pull-tab store-key="example-pull-tab-filters">
145
+
146
+ <zn-tile private caption="Leslie Alexander" description="Leslie.alexander@example.com" href="https://google.com">
147
+ <zn-icon slot="image" size="40" src="https://i.pravatar.cc/60?img=70" round></zn-icon>
148
+ <div slot="actions">
149
+ <zn-button href="#">Action</zn-button>
150
+ </div>
151
+ </zn-tile>
152
+
153
+ <zn-tile private caption="Leslie Alexander" description="Leslie.alexander@example.com" href="https://google.com">
154
+ <zn-icon slot="image" size="40" src="https://i.pravatar.cc/60?img=70" round></zn-icon>
155
+ <div slot="actions">
156
+ <zn-button href="#">Action</zn-button>
157
+ </div>
158
+ </zn-tile>
159
+
160
+ <zn-tile original caption="Leslie Alexander" description="Leslie.alexander@example.com" href="https://google.com">
161
+ <zn-icon slot="image" size="40" src="https://i.pravatar.cc/60?img=70" round></zn-icon>
162
+ <div slot="actions">
163
+ <zn-button href="#">Action</zn-button>
164
+ </div>
165
+ </zn-tile>
166
+
167
+ <div slot="filter" attribute="private" default="true">Toggle Private Messages</div>
168
+ <div slot="filter" attribute="original" default="true">Toggle Original Messages</div>
169
+
170
+ </zn-settings-container>
171
+ </div>
172
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.29",
3
+ "version": "1.1.31",
4
4
  "description": "A collection of web components for building web applications based off of @shoelace-style/Shoelace",
5
5
  "keywords": [
6
6
  "web components",
@@ -127,7 +127,8 @@
127
127
  &__right {
128
128
  flex-grow: 1;
129
129
  flex-shrink: 1;
130
- overflow: hidden;
130
+ overflow-x: clip;
131
+ overflow-y: visible;
131
132
  min-width: 20px;
132
133
  }
133
134
 
@@ -225,6 +226,7 @@
225
226
  slot[name="actions"] {
226
227
  justify-content: end;
227
228
  flex-wrap: nowrap;
229
+ height: var(--zn-icon-size, 20px);
228
230
  }
229
231
 
230
232
  slot[name="actions"]::slotted(zn-button-menu) {
@@ -51,6 +51,7 @@
51
51
 
52
52
  &::part(base) {
53
53
  border-bottom: 0;
54
+ overflow: visible;
54
55
  }
55
56
 
56
57
  &--inline {
@@ -25,10 +25,13 @@ interface SettingsContainerFilter {
25
25
  *
26
26
  * @dependency zn-example
27
27
  *
28
- * @event zn-event-name - Emitted as an example.
28
+ * @event zn-show - Emitted when the pull tab panel opens.
29
+ * @event zn-hide - Emitted when the pull tab panel closes.
29
30
  *
30
31
  * @slot - The default slot.
31
- * @slot example - An example slot.
32
+ * @slot filter - Filter definitions used to toggle visibility of content.
33
+ * @slot dropdown-content - Custom content for the settings panel.
34
+ * @slot tab - Extra content rendered inside the pull tab next to the chevron.
32
35
  *
33
36
  * @csspart base - The component's base wrapper.
34
37
  *
@@ -43,6 +46,10 @@ export default class ZnSettingsContainer extends ZincElement {
43
46
 
44
47
  @property({attribute: 'store-key'}) storeKey: string;
45
48
  @property({attribute: 'no-scroll'}) noScroll: boolean;
49
+ @property({attribute: 'pull-tab', type: Boolean, reflect: true}) pullTab = false;
50
+ @property({type: Boolean, reflect: true}) open = false;
51
+
52
+ @state() private panelExpanded = false;
46
53
 
47
54
  private readonly _mutationObserver = new MutationController(this, {
48
55
  target: null,
@@ -88,6 +95,40 @@ export default class ZnSettingsContainer extends ZincElement {
88
95
  this._mutationObserver.observe(this);
89
96
  }
90
97
 
98
+ disconnectedCallback() {
99
+ document.removeEventListener('mousedown', this.handleDocumentMouseDown, true);
100
+ super.disconnectedCallback();
101
+ }
102
+
103
+ private handleDocumentMouseDown = (e: MouseEvent) => {
104
+ if (!this.open) return;
105
+ const pullTab = this.shadowRoot?.querySelector('.pull-tab');
106
+ if (pullTab && e.composedPath().includes(pullTab)) return;
107
+ this.hidePullTab();
108
+ };
109
+
110
+ private togglePullTab = () => {
111
+ if (this.open) {
112
+ this.hidePullTab();
113
+ } else {
114
+ this.open = true;
115
+ document.addEventListener('mousedown', this.handleDocumentMouseDown, true);
116
+ this.emit('zn-show');
117
+ }
118
+ };
119
+
120
+ private hidePullTab() {
121
+ this.open = false;
122
+ this.panelExpanded = false;
123
+ document.removeEventListener('mousedown', this.handleDocumentMouseDown, true);
124
+ this.emit('zn-hide');
125
+ }
126
+
127
+ private handlePanelTransitionEnd = (e: TransitionEvent) => {
128
+ if (e.propertyName !== 'grid-template-rows') return;
129
+ this.panelExpanded = this.open;
130
+ };
131
+
91
132
  // Debounced scheduling to avoid excessive updates on rapid DOM mutations
92
133
  private scheduleUpdateFilters() {
93
134
  if (this._updateFiltersScheduled) return;
@@ -221,17 +262,41 @@ export default class ZnSettingsContainer extends ZincElement {
221
262
  <div class="scroll-content">
222
263
  <slot @slotchange="${this.handleContentSlotChange}"></slot>
223
264
  </div>
224
- <zn-dropdown placement="${placement}" class="${classMap({
225
- 'setting-container__dropdown': true,
226
- [`setting-container__dropdown--${this.position}`]: true,
227
- })}">
228
- <zn-icon class="setting-container__toggle-button" slot="trigger" size="24" src="settings" round></zn-icon>
229
- ${this.getDropdownContent()}
230
- </zn-dropdown>
265
+ ${this.pullTab ? this.renderPullTab() : html`
266
+ <zn-dropdown placement="${placement}" class="${classMap({
267
+ 'setting-container__dropdown': true,
268
+ [`setting-container__dropdown--${this.position}`]: true,
269
+ })}">
270
+ <zn-icon class="setting-container__toggle-button" slot="trigger" size="24" src="settings" round></zn-icon>
271
+ ${this.getDropdownContent()}
272
+ </zn-dropdown>`}
231
273
  </div>
232
274
  `;
233
275
  }
234
276
 
277
+ private renderPullTab() {
278
+ return html`
279
+ <div class="${classMap({
280
+ 'pull-tab': true,
281
+ 'pull-tab--open': this.open,
282
+ 'pull-tab--expanded': this.panelExpanded,
283
+ })}">
284
+ <div class="pull-tab__panel" @transitionend="${this.handlePanelTransitionEnd}">
285
+ <div class="pull-tab__panel-content">
286
+ ${this.getDropdownContent()}
287
+ </div>
288
+ </div>
289
+ <button class="pull-tab__tab"
290
+ type="button"
291
+ aria-expanded="${this.open ? 'true' : 'false'}"
292
+ aria-label="Settings"
293
+ @click="${this.togglePullTab}">
294
+ <slot name="tab"></slot>
295
+ <zn-icon src="${this.open ? 'chevron-up@lu' : 'chevron-down@lu'}" size="20"></zn-icon>
296
+ </button>
297
+ </div>`;
298
+ }
299
+
235
300
  private getDropdownContent() {
236
301
  const hasDropdownContent = this.querySelector('[slot="dropdown-content"]');
237
302
  if (hasDropdownContent) {
@@ -84,6 +84,81 @@ zn-button {
84
84
  border-radius: var(--zn-border-radius);
85
85
  }
86
86
 
87
+ .pull-tab {
88
+ position: absolute;
89
+ top: 0;
90
+ right: calc(var(--zn-base-gap) + var(--zn-gutter));
91
+ z-index: 20;
92
+ display: flex;
93
+ flex-direction: column;
94
+ align-items: flex-end;
95
+ max-height: 100%;
96
+
97
+ &__panel {
98
+ display: grid;
99
+ grid-template-rows: 0fr;
100
+ min-height: 0;
101
+ overflow: hidden;
102
+ background-color: rgb(var(--zn-panel));
103
+ border: 1px solid rgb(var(--zn-border-color));
104
+ border-top: none;
105
+ border-bottom-width: 0;
106
+ border-radius: 0 0 0 var(--zn-border-radius);
107
+ transition: grid-template-rows 0.22s ease, border-bottom-width 0s linear 0.22s;
108
+ will-change: grid-template-rows;
109
+ }
110
+
111
+ &--open &__panel {
112
+ grid-template-rows: 1fr;
113
+ border-bottom-width: 1px;
114
+ transition: grid-template-rows 0.22s ease, border-bottom-width 0s linear 0s;
115
+ }
116
+
117
+ &__panel-content {
118
+ min-height: 0;
119
+ overflow: hidden;
120
+
121
+ .settings-container__dropdown-content {
122
+ border: none;
123
+ box-shadow: none;
124
+ border-radius: 0;
125
+ margin-bottom: 0;
126
+ max-width: none;
127
+ width: auto;
128
+ }
129
+ }
130
+
131
+ &--expanded &__panel-content {
132
+ overflow-y: auto;
133
+ }
134
+
135
+ &__tab {
136
+ flex-shrink: 0;
137
+ background-color: rgb(var(--zn-panel));
138
+ color: rgb(var(--zn-text-heading));
139
+ border: 1px solid rgb(var(--zn-border-color));
140
+ border-top: none;
141
+ border-radius: 0 0 10px 10px;
142
+ padding: var(--zn-spacing-x-small);
143
+ cursor: pointer;
144
+ display: inline-flex;
145
+ align-items: center;
146
+ gap: var(--zn-spacing-x-small);
147
+ max-height: 36px;
148
+ overflow: hidden;
149
+ margin-top: -1px;
150
+
151
+ &:hover {
152
+ background-color: rgb(var(--zn-shadow));
153
+ }
154
+
155
+ &:focus-visible {
156
+ outline: 2px solid rgb(var(--zn-primary));
157
+ outline-offset: -2px;
158
+ }
159
+ }
160
+ }
161
+
87
162
  .settings-container__dropdown-content {
88
163
  display: flex;
89
164
  flex-direction: column;
@@ -7,4 +7,39 @@ describe('<zn-settings-container>', () => {
7
7
 
8
8
  expect(el).to.exist;
9
9
  });
10
+
11
+ describe('pull tab', () => {
12
+ it('should render a pull tab instead of the dropdown', async () => {
13
+ const el = await fixture<HTMLElement>(html` <zn-settings-container pull-tab></zn-settings-container> `);
14
+
15
+ expect(el.shadowRoot!.querySelector('.pull-tab')).to.exist;
16
+ expect(el.shadowRoot!.querySelector('zn-dropdown')).to.not.exist;
17
+ });
18
+
19
+ it('should open and close when the tab is clicked', async () => {
20
+ const el = await fixture<HTMLElement>(html` <zn-settings-container pull-tab></zn-settings-container> `);
21
+ const tab = el.shadowRoot!.querySelector<HTMLButtonElement>('.pull-tab__tab')!;
22
+
23
+ tab.click();
24
+ await (el as any).updateComplete;
25
+ expect(el.hasAttribute('open')).to.be.true;
26
+
27
+ tab.click();
28
+ await (el as any).updateComplete;
29
+ expect(el.hasAttribute('open')).to.be.false;
30
+ });
31
+
32
+ it('should close when clicking outside', async () => {
33
+ const el = await fixture<HTMLElement>(html` <zn-settings-container pull-tab></zn-settings-container> `);
34
+ const tab = el.shadowRoot!.querySelector<HTMLButtonElement>('.pull-tab__tab')!;
35
+
36
+ tab.click();
37
+ await (el as any).updateComplete;
38
+ expect(el.hasAttribute('open')).to.be.true;
39
+
40
+ document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, composed: true }));
41
+ await (el as any).updateComplete;
42
+ expect(el.hasAttribute('open')).to.be.false;
43
+ });
44
+ });
10
45
  });