Skip to content

Commit 286bc1f

Browse files
Joseph BennettJoseph Bennett
authored andcommitted
Add submenu items
1 parent 9747934 commit 286bc1f

6 files changed

Lines changed: 277 additions & 141 deletions

File tree

src/components/menuitem.js

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import win98Styles from '../css/98-overrides.css?inline';
2+
3+
/**
4+
* Win98MenuItem - A single item within a start menu or submenu.
5+
* Supports icons, labels, and nested submenus.
6+
*/
7+
class Win98MenuItem extends HTMLElement {
8+
constructor() {
9+
super();
10+
this.attachShadow({ mode: 'open' });
11+
}
12+
13+
static get observedAttributes() {
14+
return ['label', 'icon', 'has-submenu', 'large'];
15+
}
16+
17+
connectedCallback() {
18+
this.render();
19+
this.setupListeners();
20+
this.updateSubmenuState();
21+
}
22+
23+
attributeChangedCallback() {
24+
this.render();
25+
this.updateSubmenuState();
26+
}
27+
28+
setupListeners() {
29+
this.shadowRoot.addEventListener('click', (e) => {
30+
if (!this.hasSubmenu) {
31+
this.dispatchEvent(new CustomEvent('menu-item-click', {
32+
bubbles: true,
33+
composed: true,
34+
detail: { label: this.getAttribute('label') }
35+
}));
36+
}
37+
});
38+
39+
const submenuSlot = this.shadowRoot.querySelector('slot[name="submenu"]');
40+
if (submenuSlot) {
41+
submenuSlot.addEventListener('slotchange', () => this.updateSubmenuState());
42+
}
43+
}
44+
45+
updateSubmenuState() {
46+
const submenuSlot = this.shadowRoot.querySelector('slot[name="submenu"]');
47+
const hasContent = submenuSlot && submenuSlot.assignedElements().length > 0;
48+
this.hasSubmenu = hasContent || this.hasAttribute('has-submenu');
49+
50+
const arrow = this.shadowRoot.querySelector('.menu-item-arrow');
51+
if (arrow) {
52+
arrow.style.display = this.hasSubmenu ? 'block' : 'none';
53+
}
54+
55+
const container = this.shadowRoot.querySelector('.submenu-container');
56+
if (container) {
57+
container.classList.toggle('has-items', this.hasSubmenu);
58+
}
59+
}
60+
61+
static get componentStyles() {
62+
return `
63+
:host {
64+
display: block;
65+
position: relative;
66+
font-family: "Pixelated MS Sans Serif", Arial, sans-serif;
67+
font-size: 11px;
68+
}
69+
70+
.menu-item {
71+
display: flex;
72+
align-items: center;
73+
padding: 0 4px 0 6px;
74+
cursor: default;
75+
color: black;
76+
height: 22px; /* Win98 items are generally around 22-24px */
77+
white-space: nowrap;
78+
position: relative;
79+
}
80+
81+
:host([large]) .menu-item {
82+
height: 38px; /* Taller for main menu */
83+
padding: 0 8px;
84+
font-size: 12px;
85+
}
86+
87+
:host(:hover) > .menu-item {
88+
background-color: #000080;
89+
color: white;
90+
}
91+
92+
.menu-item-icon {
93+
width: 16px;
94+
height: 16px;
95+
margin-right: 8px;
96+
display: flex;
97+
align-items: center;
98+
justify-content: center;
99+
flex-shrink: 0;
100+
}
101+
102+
:host([large]) .menu-item-icon {
103+
width: 32px;
104+
height:32px;
105+
}
106+
107+
.menu-item-icon img, ::slotted([slot="icon"]) {
108+
max-width: 100%;
109+
max-height: 100%;
110+
image-rendering: pixelated;
111+
}
112+
113+
.menu-item-text {
114+
flex: 1;
115+
white-space: nowrap;
116+
padding-right: 16px;
117+
}
118+
119+
.menu-item-arrow {
120+
width: 0;
121+
height: 0;
122+
border-top: 4px solid transparent;
123+
border-bottom: 4px solid transparent;
124+
border-left: 4px solid black;
125+
margin-left: auto;
126+
display: none; /* Shown via JS if submenu exists */
127+
}
128+
129+
:host(:hover) > .menu-item > .menu-item-arrow {
130+
border-left-color: white;
131+
}
132+
133+
.submenu-container {
134+
display: none;
135+
position: absolute;
136+
left: calc(100% - 1px);
137+
top: 0;
138+
background: #c0c0c0;
139+
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #dfdfdf, inset -2px -2px grey, inset 2px 2px #fff;
140+
padding: 2px;
141+
min-width: 120px;
142+
z-index: 1000;
143+
width: max-content; /* Fit content */
144+
}
145+
146+
:host([large]) .submenu-container {
147+
left: 100%;
148+
top: -2px;
149+
}
150+
151+
:host(:hover) > .submenu-container.has-items {
152+
display: block;
153+
}
154+
`;
155+
}
156+
157+
render() {
158+
const label = this.getAttribute('label') || '';
159+
const icon = this.getAttribute('icon') || '';
160+
161+
const win98Sheet = new CSSStyleSheet();
162+
win98Sheet.replaceSync(win98Styles);
163+
164+
const componentSheet = new CSSStyleSheet();
165+
componentSheet.replaceSync(Win98MenuItem.componentStyles);
166+
167+
this.shadowRoot.adoptedStyleSheets = [win98Sheet, componentSheet];
168+
169+
this.shadowRoot.innerHTML = `
170+
<div class="menu-item" role="menuitem">
171+
<div class="menu-item-icon">
172+
${icon ? `<img src="${icon}" alt="">` : ''}
173+
<slot name="icon"></slot>
174+
</div>
175+
<div class="menu-item-text">
176+
${label}
177+
<slot></slot>
178+
</div>
179+
<div class="menu-item-arrow"></div>
180+
</div>
181+
<div class="submenu-container" role="menu">
182+
<slot name="submenu"></slot>
183+
</div>
184+
`;
185+
}
186+
}
187+
188+
customElements.define('win98-menu-item', Win98MenuItem);
189+
export default Win98MenuItem;

src/components/menuseparator.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import win98Styles from '../css/98-overrides.css?inline';
2+
3+
/**
4+
* Win98MenuSeparator - A visual separator line for menus.
5+
*/
6+
class Win98MenuSeparator extends HTMLElement {
7+
constructor() {
8+
super();
9+
this.attachShadow({ mode: 'open' });
10+
}
11+
12+
connectedCallback() {
13+
this.render();
14+
}
15+
16+
static get componentStyles() {
17+
return `
18+
:host {
19+
display: block;
20+
height: 2px;
21+
margin: 3px 1px;
22+
background: white;
23+
border-top: 1px solid gray;
24+
}
25+
`;
26+
}
27+
28+
render() {
29+
const win98Sheet = new CSSStyleSheet();
30+
win98Sheet.replaceSync(win98Styles);
31+
32+
const componentSheet = new CSSStyleSheet();
33+
componentSheet.replaceSync(Win98MenuSeparator.componentStyles);
34+
35+
this.shadowRoot.adoptedStyleSheets = [win98Sheet, componentSheet];
36+
this.shadowRoot.innerHTML = ''; // Just the :host styles are needed
37+
}
38+
}
39+
40+
customElements.define('win98-menu-separator', Win98MenuSeparator);
41+
export default Win98MenuSeparator;

src/css/98-overrides.css

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -82,53 +82,6 @@ option {
8282
height: 100%;
8383
}
8484

85-
/* Start Menu Items */
86-
.menu-item {
87-
display: flex;
88-
align-items: center;
89-
padding: 4px 8px;
90-
cursor: default;
91-
color: black;
92-
height: 24px;
93-
}
94-
95-
.menu-item:hover {
96-
background-color: #000080;
97-
color: white;
98-
}
99-
100-
.menu-item:hover .menu-item-arrow {
101-
border-left-color: white;
102-
}
103-
104-
.menu-item-icon {
105-
width: 24px;
106-
height: 24px;
107-
margin-right: 8px;
108-
display: flex;
109-
align-items: center;
110-
justify-content: center;
111-
}
112-
113-
.menu-item-icon img {
114-
max-width: 100%;
115-
max-height: 100%;
116-
}
117-
118-
.menu-item-text {
119-
flex: 1;
120-
white-space: nowrap;
121-
}
122-
123-
.menu-item-arrow {
124-
width: 0;
125-
height: 0;
126-
border-top: 4px solid transparent;
127-
border-bottom: 4px solid transparent;
128-
border-left: 4px solid black;
129-
margin-left: 8px;
130-
}
131-
13285
.separator {
13386
height: 1px;
13487
background: white;

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,8 @@ export { default as Win98Select } from './components/select.js';
77
export { default as Win98Desktop } from './components/desktop.js';
88
export { default as Win98Taskbar } from './components/taskbar.js';
99
export { default as Win98StartMenu } from './components/startmenu.js';
10+
export { default as Win98MenuItem } from './components/menuitem.js';
11+
export { default as Win98MenuSeparator } from './components/menuseparator.js';
12+
1013

1114
export { windowManager } from './services/WindowManager.js';

test/component-library.js

Lines changed: 22 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -239,53 +239,28 @@ document.getElementById('nav-start-menu')?.addEventListener('click', (e) => {
239239
div.innerHTML = `
240240
<p>The Start Menu component with slotted content.</p>
241241
<win98-start-menu visible style="position: absolute; top: 50px; left: 20px; z-index: auto;">
242-
<div class="menu-item" role="menuitem">
243-
<div class="menu-item-icon"></div>
244-
<div class="menu-item-text">Windows Update</div>
245-
</div>
246-
<div class="separator" role="separator"></div>
247-
<div class="menu-item" role="menuitem" aria-haspopup="true">
248-
<div class="menu-item-icon"></div>
249-
<div class="menu-item-text">Programs</div>
250-
<div class="menu-item-arrow"></div>
251-
</div>
252-
<div class="menu-item" role="menuitem" aria-haspopup="true">
253-
<div class="menu-item-icon"></div>
254-
<div class="menu-item-text">Favorites</div>
255-
<div class="menu-item-arrow"></div>
256-
</div>
257-
<div class="menu-item" role="menuitem" aria-haspopup="true">
258-
<div class="menu-item-icon"></div>
259-
<div class="menu-item-text">Documents</div>
260-
<div class="menu-item-arrow"></div>
261-
</div>
262-
<div class="menu-item" role="menuitem" aria-haspopup="true">
263-
<div class="menu-item-icon"></div>
264-
<div class="menu-item-text">Settings</div>
265-
<div class="menu-item-arrow"></div>
266-
</div>
267-
<div class="menu-item" role="menuitem" aria-haspopup="true">
268-
<div class="menu-item-icon"></div>
269-
<div class="menu-item-text">Find</div>
270-
<div class="menu-item-arrow"></div>
271-
</div>
272-
<div class="menu-item" role="menuitem">
273-
<div class="menu-item-icon"></div>
274-
<div class="menu-item-text">Help</div>
275-
</div>
276-
<div class="menu-item" role="menuitem">
277-
<div class="menu-item-icon"></div>
278-
<div class="menu-item-text">Run...</div>
279-
</div>
280-
<div class="separator" role="separator"></div>
281-
<div class="menu-item" role="menuitem">
282-
<div class="menu-item-icon"></div>
283-
<div class="menu-item-text">Log Off...</div>
284-
</div>
285-
<div class="menu-item" role="menuitem">
286-
<div class="menu-item-icon"></div>
287-
<div class="menu-item-text">Shut Down...</div>
288-
</div>
242+
<win98-menu-item label="Windows Update" large></win98-menu-item>
243+
<win98-menu-separator></win98-menu-separator>
244+
<win98-menu-item label="Programs" large>
245+
<div slot="submenu">
246+
<win98-menu-item label="Accessories">
247+
<div slot="submenu">
248+
<win98-menu-item label="Calculator"></win98-menu-item>
249+
<win98-menu-item label="Notepad"></win98-menu-item>
250+
</div>
251+
</win98-menu-item>
252+
<win98-menu-item label="MS-DOS Prompt"></win98-menu-item>
253+
</div>
254+
</win98-menu-item>
255+
<win98-menu-item label="Favorites" large></win98-menu-item>
256+
<win98-menu-item label="Documents" large></win98-menu-item>
257+
<win98-menu-item label="Settings" large></win98-menu-item>
258+
<win98-menu-item label="Find" large></win98-menu-item>
259+
<win98-menu-item label="Help" large></win98-menu-item>
260+
<win98-menu-item label="Run..." large></win98-menu-item>
261+
<win98-menu-separator></win98-menu-separator>
262+
<win98-menu-item label="Log Off..." large></win98-menu-item>
263+
<win98-menu-item label="Shut Down..." large></win98-menu-item>
289264
</win98-start-menu>
290265
`;
291266
stage.appendChild(div);

0 commit comments

Comments
 (0)