diff --git a/packages/outline-nav-menu/index.ts b/packages/outline-nav-menu/index.ts new file mode 100644 index 000000000..fd28fa77b --- /dev/null +++ b/packages/outline-nav-menu/index.ts @@ -0,0 +1,3 @@ +export { OutlineNav } from './src/outline-nav'; +export { OutlineNavLink } from './src/outline-nav-link/outline-nav-link'; +export { OutlineNavItem } from './src/outline-nav-item/outline-nav-item'; diff --git a/packages/outline-nav-menu/package.json b/packages/outline-nav-menu/package.json new file mode 100644 index 000000000..442c99ff9 --- /dev/null +++ b/packages/outline-nav-menu/package.json @@ -0,0 +1,46 @@ +{ + "name": "@phase2/outline-nav-menu", + "version": "0.1.3", + "description": "The Outline Components navigation menu component", + "keywords": [ + "outline", + "web-components", + "design system", + "main navigation", + "mega menu" + ], + "main": "index.ts", + "types": "index.ts", + "typings": "index.d.ts", + "files": [ + "/dist/", + "/src/", + "!/dist/tsconfig.build.tsbuildinfo" + ], + "author": "Phase2 Technology", + "repository": { + "type": "git", + "url": "https://github.com/phase2/outline.git", + "directory": "packages/outline-nav-menu" + }, + "license": "BSD-3-Clause", + "scripts": { + "build": "node ../../scripts/build.js", + "package": "yarn publish" + }, + "dependencies": { + "@phase2/outline-core": "^0.1.9", + "@types/lodash": "^4.14.192", + "@types/lodash-es": "^4.17.7", + "lit": "^2.3.1", + "lodash-es": "^4.17.21", + "tslib": "^2.1.0" + }, + "devDependencies": {}, + "publishConfig": { + "access": "public" + }, + "exports": { + ".": "./index.ts" + } +} diff --git a/packages/outline-nav-menu/src/nav-test-data.ts b/packages/outline-nav-menu/src/nav-test-data.ts new file mode 100644 index 000000000..2730dbd27 --- /dev/null +++ b/packages/outline-nav-menu/src/nav-test-data.ts @@ -0,0 +1,899 @@ +export default [ + { + text: 'Residents', + sub: [ + { + text: 'Find', + sub: [ + { + text: 'COVID-19 Information', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: "Farmer's Markets", + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Hospitals', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Libraries', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'My Supervisorial District', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Parks', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Public WiFi Locations', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Recycling Drop-off', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Services Near Me (SMC Connect)', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Voting Information', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + ], + }, + { + text: 'Find', + sub: [ + { + text: 'COVID-19 Information', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: "Farmer's Markets", + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Hospitals', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Libraries', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'My Supervisorial District', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Parks', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Public WiFi Locations', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Recycling Drop-off', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Services Near Me (SMC Connect)', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Voting Information', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + ], + }, + ], + }, + { + text: 'Business', + sub: [ + { + text: 'Find', + sub: [ + { + text: 'COVID-19 Information', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: "Farmer's Markets", + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Hospitals', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Libraries', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'My Supervisorial District', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Parks', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Public WiFi Locations', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Recycling Drop-off', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Services Near Me (SMC Connect)', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Voting Information', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + ], + }, + { + text: 'Find', + sub: [ + { + text: 'COVID-19 Information', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: "Farmer's Markets", + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Hospitals', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Libraries', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'My Supervisorial District', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Parks', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Public WiFi Locations', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Recycling Drop-off', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Services Near Me (SMC Connect)', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + { + text: 'Voting Information', + url: '#', + sub: [ + { text: 'COVID-19 Information', url: '#' }, + { text: "Farmer's Markets", url: '#' }, + { text: 'Hospitals', url: '#' }, + { text: 'Libraries', url: '#' }, + { text: 'My Supervisorial District', url: '#' }, + { text: 'Parks', url: '#' }, + { text: 'Public WiFi Locations', url: '#' }, + { text: 'Recycling Drop-off', url: '#' }, + { text: 'Services Near Me (SMC Connect)', url: '#' }, + { text: 'Voting Information', url: '#' }, + ], + }, + ], + }, + ], + }, +]; + +/* eslint-disable no-useless-escape */ +export const jsonData = { + jsonapi: { + version: '1.0', + meta: { links: { self: { href: 'http://jsonapi.org/format/1.0/' } } }, + }, + data: [ + { + type: 'menu_link_content--menu_link_content', + id: 'menu_link_content:85f93084-c7d2-4923-9b04-8fbb773a8606', + attributes: { + description: null, + enabled: true, + expanded: false, + menu_name: 'main', + meta: { entity_id: '1' }, + options: [], + parent: '', + provider: 'menu_link_content', + route: { name: '\u003Cnone\u003E', parameters: [] }, + title: 'Main 1', + url: '', + weight: 0, + }, + }, + { + type: 'menu_link_content--menu_link_content', + id: 'menu_link_content:60b33442-f772-42d6-a812-cc19351e939a', + attributes: { + description: null, + enabled: true, + expanded: false, + menu_name: 'main', + meta: { entity_id: '5' }, + options: { external: true }, + parent: 'menu_link_content:85f93084-c7d2-4923-9b04-8fbb773a8606', + provider: 'menu_link_content', + route: { name: '', parameters: [] }, + title: 'Main 1 - sub 1', + url: 'https://www.npr.org', + weight: -50, + }, + }, + { + type: 'menu_link_content--menu_link_content', + id: 'menu_link_content:faa60829-fcd4-400c-8e86-54989f1f29e4', + attributes: { + description: null, + enabled: true, + expanded: false, + menu_name: 'main', + meta: { entity_id: '6' }, + options: { external: true }, + parent: 'menu_link_content:85f93084-c7d2-4923-9b04-8fbb773a8606', + provider: 'menu_link_content', + route: { name: '', parameters: [] }, + title: 'Main 1 - sub 2', + url: 'https://www.cnn.com', + weight: 0, + }, + }, + { + type: 'menu_link_content--menu_link_content', + id: 'menu_link_content:dacbe9fb-f3de-430c-b28a-ff12ebe2deb8', + attributes: { + description: null, + enabled: true, + expanded: false, + menu_name: 'main', + meta: { entity_id: '7' }, + options: { external: true }, + parent: 'menu_link_content:85f93084-c7d2-4923-9b04-8fbb773a8606', + provider: 'menu_link_content', + route: { name: '', parameters: [] }, + title: 'Main 1 - sub 3', + url: 'https://www.cnn.com', + weight: 0, + }, + }, + { + type: 'menu_link_content--menu_link_content', + id: 'menu_link_content:ad0e49f8-0e55-44a9-93fa-55683e56af03', + attributes: { + description: null, + enabled: true, + expanded: false, + menu_name: 'main', + meta: { entity_id: '2' }, + options: [], + parent: '', + provider: 'menu_link_content', + route: { name: '\u003Cnone\u003E', parameters: [] }, + title: 'Main 2', + url: '', + weight: 0, + }, + }, + { + type: 'menu_link_content--menu_link_content', + id: 'menu_link_content:09a95b3b-8514-4fc4-a661-e149fdd5fdb2', + attributes: { + description: null, + enabled: true, + expanded: false, + menu_name: 'main', + meta: { entity_id: '9' }, + options: { external: true }, + parent: 'menu_link_content:ad0e49f8-0e55-44a9-93fa-55683e56af03', + provider: 'menu_link_content', + route: { name: '', parameters: [] }, + title: 'Main 2 - sub 1', + url: 'https://www.opb.org', + weight: 0, + }, + }, + { + type: 'menu_link_content--menu_link_content', + id: 'menu_link_content:313f3a49-d04d-422b-afb2-f8a28d1136dc', + attributes: { + description: null, + enabled: true, + expanded: false, + menu_name: 'main', + meta: { entity_id: '8' }, + options: [], + parent: 'menu_link_content:ad0e49f8-0e55-44a9-93fa-55683e56af03', + provider: 'menu_link_content', + route: { name: '\u003Cnone\u003E', parameters: [] }, + title: 'Main 2 - sub 2', + url: '', + weight: 0, + }, + }, + { + type: 'menu_link_content--menu_link_content', + id: 'menu_link_content:0ad5d5f7-c471-4a4c-9bd7-dbe365b50ccc', + attributes: { + description: null, + enabled: true, + expanded: false, + menu_name: 'main', + meta: { entity_id: '11' }, + options: [], + parent: 'menu_link_content:313f3a49-d04d-422b-afb2-f8a28d1136dc', + provider: 'menu_link_content', + route: { name: '\u003Cnone\u003E', parameters: [] }, + title: 'Main 2 - sub 2 - sub 2/1', + url: '', + weight: 0, + }, + }, + { + type: 'menu_link_content--menu_link_content', + id: 'menu_link_content:a738378b-441c-4702-958a-1dc6a3982323', + attributes: { + description: null, + enabled: true, + expanded: false, + menu_name: 'main', + meta: { entity_id: '10' }, + options: [], + parent: 'menu_link_content:ad0e49f8-0e55-44a9-93fa-55683e56af03', + provider: 'menu_link_content', + route: { name: '\u003Cnone\u003E', parameters: [] }, + title: 'Main 2 - sub 3', + url: '', + weight: 0, + }, + }, + { + type: 'menu_link_content--menu_link_content', + id: 'menu_link_content:5bb9569d-0dd1-445d-af9b-ecea58f7ad6d', + attributes: { + description: null, + enabled: true, + expanded: false, + menu_name: 'main', + meta: { entity_id: '3' }, + options: [], + parent: '', + provider: 'menu_link_content', + route: { name: '\u003Cnone\u003E', parameters: [] }, + title: 'Main 3', + url: '', + weight: 0, + }, + }, + { + type: 'menu_link_content--menu_link_content', + id: 'menu_link_content:5ff49b41-fca4-4889-b974-9f7dbfdc0ad6', + attributes: { + description: null, + enabled: true, + expanded: false, + menu_name: 'main', + meta: { entity_id: '12' }, + options: [], + parent: 'menu_link_content:5bb9569d-0dd1-445d-af9b-ecea58f7ad6d', + provider: 'menu_link_content', + route: { name: '\u003Cnone\u003E', parameters: [] }, + title: 'Main 3 - sub 1', + url: '', + weight: 0, + }, + }, + { + type: 'menu_link_content--menu_link_content', + id: 'menu_link_content:5bc35f9b-5ddb-41fd-a16c-03af661781a2', + attributes: { + description: null, + enabled: true, + expanded: false, + menu_name: 'main', + meta: { entity_id: '4' }, + options: { external: true }, + parent: '', + provider: 'menu_link_content', + route: { name: '', parameters: [] }, + title: 'Main 4', + url: 'https://www.youtube.com', + weight: 0, + }, + }, + ], + links: { self: { href: 'http://demo.docksal.site/jsonapi/menu_items/main' } }, +}; diff --git a/packages/outline-nav-menu/src/outline-nav-item/outline-nav-item.css b/packages/outline-nav-menu/src/outline-nav-item/outline-nav-item.css new file mode 100644 index 000000000..b8ede5cef --- /dev/null +++ b/packages/outline-nav-menu/src/outline-nav-item/outline-nav-item.css @@ -0,0 +1,120 @@ +ul { + @apply m-0 p-0; +} + +li { + @apply list-none !important; +} + +a { + @apply no-underline; + color: unset; +} + +.list-item { + @apply relative flex w-full flex-col py-2 px-4 text-outline-blue-500; + border-bottom: 1px solid var(--blue-500); + + & a { + @apply text-outline-black no-underline !important; + } + + & li { + @apply list-none text-outline-black !important; + } +} + +.list-item--main { + @apply relative mt-2 flex w-full justify-center rounded-t-md px-3 pb-2 pt-8 text-xl text-outline-white; +} + +.list-item--main[active], +.list-item[active] { + @apply bg-outline-white; +} + +.list-item[active] { + border-bottom: 1px solid var(--blue-900); +} + +.list-item--main[active] > span, +.list-item--main[active] > a { + @apply text-outline-blue-500; +} + +.list-item[active] > span, +.list-item[active] > a { + @apply text-outline-blue-900; +} + +.list-item--main[active] > .list-item--sub-menu, +.list-item[active] > .list-item--sub-menu { + @apply flex; +} + +.list-item--button { + display: flex; + justify-content: space-between; + background: none; + border: none; + align-items: center; + max-width: fit-content; +} + +.list-item--label { + @apply m-0; +} + +.list-item--sub-menu { + @apply absolute top-full hidden w-full min-w-max list-none flex-col p-0; +} + +.list-item[active] > .list-item--sub-menu { + @apply left-full top-0; +} + +/* Mobile */ +.list-item.mobile, +.list-item--main.mobile { + @apply flex w-full flex-row justify-between; + transition: 0.5s; +} + +.list-item--main.mobile[active] > span, +.list-item--main.mobile[active] > a, +.list-item.mobile[active] > a, +.list-item.mobile[active] > span { + @apply text-outline-blue-900; +} + +.list-item--main.mobile { + @apply rounded-none; +} + +.list-item--main.mobile[active] > .list-item--sub-menu, +.list-item.mobile[active] > .list-item--sub-menu { + @apply left-0 top-full min-w-min; +} + +.list-item--main.mobile[active] > .list-item--sub-menu { + @apply pl-3; + width: calc(100vw - 43px); +} + +.list-item.mobile[active] > .list-item--sub-menu { + width: calc(100vw - 56px); +} + +/* Utility */ + +.flyaway { + transform: translateX(-150%); + visibility: hidden; + opacity: 0; + @apply m-0 h-0 border-0 p-0 !important; +} + +/* makes sure pointer events register on the button not the icon. */ +.list-item--button-icon { + pointer-events: none; +} diff --git a/packages/outline-nav-menu/src/outline-nav-item/outline-nav-item.ts b/packages/outline-nav-menu/src/outline-nav-item/outline-nav-item.ts new file mode 100644 index 000000000..1239d38a9 --- /dev/null +++ b/packages/outline-nav-menu/src/outline-nav-item/outline-nav-item.ts @@ -0,0 +1,299 @@ +import { CSSResultGroup, TemplateResult, html } from 'lit'; +import { OutlineElement, MobileController } from '@phase2/outline-core'; +import { + customElement, + property, + queryAssignedElements, +} from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { classMap } from 'lit/directives/class-map.js'; +import componentStyles from './outline-nav-item.css.lit'; +import debounce from 'lodash-es/debounce'; + +import '@phase2/outline-icon'; +import '../outline-nav-link/outline-nav-link'; +import '../outline-nav'; + +import { MenuLink, MenuNavItem } from '../types'; + +/** + * The OutlineNavItem component + * @element outline-nav-item + * @slot sub - for outline-nav-item/outline-nav-link + */ +@customElement('outline-nav-item') +export class OutlineNavItem extends OutlineElement { + static styles: CSSResultGroup = [componentStyles]; + private mobileController = new MobileController(this); + private _seed = Math.floor(Math.random() * 100000).toString(); + + @property({ type: Object }) + item: MenuNavItem; + + @property({ type: Boolean }) + toplevel: boolean; + + @property({ type: String, reflect: false }) + parentID: string; + + @property({ type: String }) + id = `list-item-${this._seed}`; + + @property({ type: String }) + text: string; + + @property({ type: String }) + url: string; + + @property({ type: Boolean }) + open = false; + + @property({ type: Boolean }) + isMobile: boolean = this.mobileController.isMobile; + + @queryAssignedElements({ slot: 'sub', flatten: true }) + subs: Array | Array; + + render(): TemplateResult { + const wrapperClasses = { + 'list-item': !this.toplevel, + 'list-item--main': this.toplevel, + 'mobile': this.isMobile, + }; + return html` +
  • + ${this.configureTemplate()} + ${this.isMobile ? this.buttonTemplate() : null} + +
  • + `; + } + + firstUpdated() { + this.handleMobileEventListener(); + window.addEventListener( + 'resize', + debounce(() => this.handleResize(), 100) + ); + } + + disconnectedCallback() { + window.removeEventListener( + 'resize', + debounce(() => this.handleResize(), 100) + ); + } + + generateListItems() { + if (this.item && this.item.sub) { + return this.item.sub.map((listItem: MenuLink | MenuNavItem) => { + const hasSubList = Object?.keys(listItem)?.includes('sub') + ? true + : false; + if (hasSubList) { + const item = listItem as MenuNavItem; + return html``; + } else { + const item = listItem as MenuLink; + return html``; + } + }); + } + return; + } + + configureTemplate() { + if (this.url || this?.item?.url) { + return this.linkTemplate(); + } + if (!this.url && !this?.item?.url) { + return this.spanTemplate(); + } + return; + } + + linkTemplate() { + return html` + + ${this.item ? this.item.text : this.text} + + `; + } + + spanTemplate() { + return html` + + ${this.item ? this.item.text : this.text} + + `; + } + + buttonTemplate() { + return html` + + `; + } + + desktopMouseOverOpen() { + if (!this.isMobile) { + this.applyActiveAttribute(); + this.open = true; + } + } + + desktopMouseOutClose() { + if (!this.isMobile) { + this.removeActiveAttribute(); + this.open = false; + } + } + + getSiblingNavItems() { + return [...this.parentElement!.children] + .map(el => el.shadowRoot?.children[0] as OutlineNavItem) + .filter(el => el.id !== this.id); + } + + applyMobileFlyout() { + this.getSiblingNavItems().map(el => el.classList.add('flyaway')); + } + + removeMobileFlyout() { + this.getSiblingNavItems().map(el => el.classList.remove('flyaway')); + } + + applyActiveAttribute() { + this.shadowRoot!.children[0].setAttribute('active', 'active'); + } + + removeActiveAttribute() { + this.shadowRoot!.children[0].removeAttribute('active'); + } + + toggleMobileFlyout() { + this.open ? this.removeMobileFlyout() : this.applyMobileFlyout(); + } + + toggleMenu() { + if (this.isMobile) { + this.toggleMobileFlyout(); + } + if (this.open) { + this.shadowRoot!.children[0].removeAttribute('active'); + this.removeActiveAttribute(); + this.open = !this.open; + } else { + this.applyActiveAttribute(); + this.shadowRoot!.children[0].setAttribute('active', 'active'); + this.open = !this.open; + } + } + + passMobileClickToButton() { + const childButton = [...this.children].filter( + el => el.localName === 'button' + )[0] as HTMLButtonElement; + childButton?.click(); + } + + handleMobileEventListener() { + if (this.mobileController.isMobile) { + const thisLiElement = this.shadowRoot!.getElementById( + this.id + ) as HTMLElement; + thisLiElement?.addEventListener('click', this.passMobileClickToButton); + } + if (!this.mobileController.isMobile) { + const thisLiElement = document.getElementById(this.id) as HTMLElement; + thisLiElement?.removeEventListener('click', this.passMobileClickToButton); + } + } + + handleToggleMenu(e: KeyboardEvent) { + const keyed = e.type === 'keydown'; + if (keyed) { + // make sure menu toggles only on Space and Enter keys + if (e.key === 'Enter' || e.key === ' ') { + // prevent scrolling on space + if (e.key === ' ') { + e.preventDefault(); + } + + this.toggleMenu(); + } + } else { + this.toggleMenu(); + } + } + + handleResize() { + const hasSwitched = this.isMobile !== this.mobileController.isMobile; + if (hasSwitched) { + this.isMobile = this.mobileController.isMobile; + this.open = false; + this.removeMobileFlyout(); + this.removeActiveAttribute(); + this.handleMobileEventListener(); + } + } +} diff --git a/packages/outline-nav-menu/src/outline-nav-link/outline-nav-link.css b/packages/outline-nav-menu/src/outline-nav-link/outline-nav-link.css new file mode 100644 index 000000000..7acffa6ac --- /dev/null +++ b/packages/outline-nav-menu/src/outline-nav-link/outline-nav-link.css @@ -0,0 +1,57 @@ +.menu-link, +.menu-link--main { + @apply flex w-full no-underline; + transition: 0; + & a { + @apply no-underline; + } + + & li { + list-style: none; + } +} + +.menu-link--main { + @apply relative mt-2 flex w-full justify-start px-3 pb-2 pt-8 text-xl text-outline-white md:justify-center md:rounded-t-md; + transition: 0s; + & a { + @apply text-outline-white; + } + + &:hover, + &:focus { + @apply md:bg-outline-white md:text-outline-blue-500; + + & a { + @apply md:text-outline-blue-500; + } + } +} + +.menu-link { + @apply py-2 px-4 text-outline-black; + border-bottom: solid 1px var(--blue-500); + + &:hover, + &:focus { + @apply bg-outline-white text-outline-blue-900; + border-bottom: solid 1px var(--blue-900); + } +} + +/* Mobile */ + +.menu-link.mobile, +.menu-link--main.mobile { + @apply w-full; + transition: 0.5s; +} + +/* Utility */ + +.flyaway { + transform: translateX(-150%); + visibility: hidden; + opacity: 0; + @apply m-0 h-0 border-0 p-0 !important; +} diff --git a/packages/outline-nav-menu/src/outline-nav-link/outline-nav-link.ts b/packages/outline-nav-menu/src/outline-nav-link/outline-nav-link.ts new file mode 100644 index 000000000..2ebceec21 --- /dev/null +++ b/packages/outline-nav-menu/src/outline-nav-link/outline-nav-link.ts @@ -0,0 +1,55 @@ +import { CSSResultGroup, TemplateResult, html } from 'lit'; +import { OutlineElement, MobileController } from '@phase2/outline-core'; +import { customElement, property, queryAssignedNodes } from 'lit/decorators.js'; +import componentStyles from './outline-nav-link.css.lit'; +import { classMap } from 'lit/directives/class-map.js'; +import { MenuLink } from '../types'; + +/** + * The OutlineNavLink component + * @element outline-nav-link + */ +@customElement('outline-nav-link') +export class OutlineNavLink extends OutlineElement { + static styles: CSSResultGroup = [componentStyles]; + private mobileController = new MobileController(this); + + @property({ type: Object }) + item: MenuLink; + + @property({ type: Boolean }) + toplevel: boolean; + + @queryAssignedNodes() + slottedLink: HTMLSlotElement[]; + + @property({ type: Boolean }) + isMobile: boolean = this.mobileController.isMobile; + + render(): TemplateResult { + const wrapperClasses = { + 'menu-link': !this.toplevel, + 'menu-link--main': this.toplevel, + 'mobile': this.isMobile, + }; + + return html` +
  • + ${ + this.item + ? html`${this.item.text}` + : null + } + +
  • + `; + } + + handleKeyDown(e?: KeyboardEvent) { + if (e?.type === 'keydown' && e.key === ' ') { + e.preventDefault(); + } + } +} diff --git a/packages/outline-nav-menu/src/outline-nav.css b/packages/outline-nav-menu/src/outline-nav.css new file mode 100644 index 000000000..d598d5520 --- /dev/null +++ b/packages/outline-nav-menu/src/outline-nav.css @@ -0,0 +1,14 @@ +.nav--main-list { + @apply relative flex w-full justify-around bg-outline-dusty-blue; +} + +/* .nav--main-list-slot::slotted(outline-nav-item), +.nav--main-list-slot::slotted(outline-nav-link) { + @apply flex w-full justify-evenly; +} */ + +/* Mobile */ + +.nav--main-list.mobile { + @apply flex-col px-0; +} diff --git a/packages/outline-nav-menu/src/outline-nav.ts b/packages/outline-nav-menu/src/outline-nav.ts new file mode 100644 index 000000000..1b3a46835 --- /dev/null +++ b/packages/outline-nav-menu/src/outline-nav.ts @@ -0,0 +1,133 @@ +import { CSSResultGroup, TemplateResult, html } from 'lit'; +import { OutlineElement, MobileController } from '@phase2/outline-core'; +import { + customElement, + property, + queryAssignedElements, +} from 'lit/decorators.js'; +import componentStyles from './outline-nav.css.lit'; +import { processMenuData } from './utility'; +import { MenuLink, MenuNavItem } from './types'; +import './outline-nav-link/outline-nav-link'; +import './outline-nav-item/outline-nav-item'; + +/** + * The OutlineNavMenu component + * @element outline-nav + */ +@customElement('outline-nav') +export class OutlineNav extends OutlineElement { + private mobileController = new MobileController(this); + private _seed = Math.floor(Math.random() * 100000).toString(); + static styles: CSSResultGroup = [componentStyles]; + + /** + * + */ + @property({ type: String }) + label = 'main menu'; + + @queryAssignedElements({ slot: 'list', flatten: true }) + slottedListItems: Array | Array; + + @property({ type: Array }) + listItems: Array; + + @property({ type: String, reflect: true }) + id = `menu-${this._seed}`; + + @property({ type: String }) + dataURL: string; + + @property({ type: Boolean }) + demo = false; + + firstUpdated() { + this.setData(); + this.setTopLevelAttribute(); + } + + render(): TemplateResult { + const isMobile = this.mobileController.isMobile; + + return html`
    + +
    `; + } + + setData() { + if ( + (this.listItems && this.listItems.length) || + (!this.listItems && !this.dataURL) + ) { + return; + } + if (!this.listItems && this.dataURL) { + this.setListItemData(this.dataURL); + } + } + + async fetchMenuData(url: string) { + try { + const response = await fetch(url); + return await response.json(); + } catch (error) { + // @ts-expect-error because-console-log + console.error(error); + return 'error'; + } + } + + async setListItemData(url: string) { + const fetchResponse = await this.fetchMenuData(url); + if (fetchResponse && fetchResponse !== 'error') { + const data = processMenuData(fetchResponse); + if (data?.length) { + this.listItems = data; + } + } + } + + setTopLevelAttribute() { + if (!this.listItems) { + [...this.slottedListItems].map(el => + // @ts-expect-error because ts + el.setAttribute('toplevel', 'toplevel') + ); + } + } + + generateListItemsFromData() { + return this.listItems?.map((listItem: MenuLink | MenuNavItem) => { + const hasSubList = Object?.keys(listItem)?.includes('sub') ? true : false; + if (hasSubList) { + const item = listItem as MenuNavItem; + return html``; + } else { + const item = listItem as MenuLink; + return html``; + } + }); + } +} diff --git a/packages/outline-nav-menu/src/types.ts b/packages/outline-nav-menu/src/types.ts new file mode 100644 index 000000000..946bce78b --- /dev/null +++ b/packages/outline-nav-menu/src/types.ts @@ -0,0 +1,38 @@ +export type MenuLink = { + text: string; + url: string; + id: string; +}; + +export type MenuNavItem = { + text: string; + id: string; + sub?: Array | undefined; + url?: string; +}; +export type MainNavData = MenuNavItem[]; + +export type JsonAPIResult = { + jsonapi: Object; + data: Array; + links: Object; +}; + +export type JsonAPIResultData = { + type: string; + id: string; + attributes: { + description: null | string; + enabled: boolean; + expanded: boolean; + menu_name: string; + meta: { [key: string]: string }; + options: Array; + parent: string; + provider: string; + route: { name: string; parameters: Array }; + title: string; + url: string; + weight: number; + }; +}; diff --git a/packages/outline-nav-menu/src/utility.ts b/packages/outline-nav-menu/src/utility.ts new file mode 100644 index 000000000..da6382ddd --- /dev/null +++ b/packages/outline-nav-menu/src/utility.ts @@ -0,0 +1,61 @@ +import { MainNavData, MenuLink, MenuNavItem, JsonAPIResult } from './types'; + +export const processMenuData = (fetchedData: JsonAPIResult) => { + const data = fetchedData.data.length > 0 ? fetchedData.data : false; + const links: MainNavData = []; + + if (data) { + data.forEach(entry => { + //set blank + const link: MenuLink | MenuNavItem = { text: '', id: '', url: undefined }; + + const recursiveParentFetch = ( + subFolder: MenuLink[] | MenuNavItem[], + parentId: string, + child: MenuLink | MenuNavItem + ): void => { + return subFolder.forEach(entry => { + if (entry.id === parentId) { + handleSubFolder(entry, child); + } + // @ts-expect-error- because-ts + if (entry.sub) { + // @ts-expect-error- because-ts + return recursiveParentFetch(entry.sub, parentId, child); + } + }); + }; + + const handleSubFolder = ( + parent: MenuNavItem, + child: MenuLink | MenuNavItem + ) => { + if (parent.sub) { + parent.sub.push(child); + } else { + parent.sub = []; + parent.sub.push(child); + } + }; + + // verify link is valid + if (entry.attributes.title && entry.id) { + link.text = entry.attributes.title; + link.id = entry.id; + if (entry.attributes.url) { + link.url = entry.attributes.url; + } + // if parent, push to parent.sub + if (entry.attributes.parent) { + const parentID = entry.attributes.parent; + recursiveParentFetch(links, parentID, link); + } else { + links.push(link); + } + } + }); + } else { + return; //bail + } + return links; +}; diff --git a/packages/outline-nav-menu/tsconfig.build.json b/packages/outline-nav-menu/tsconfig.build.json new file mode 100644 index 000000000..79e5565be --- /dev/null +++ b/packages/outline-nav-menu/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "./dist" + }, + "include": ["index.ts", "src/**/*"], + "references": [{ "path": "../outline-core/tsconfig.build.json" }] +} diff --git a/packages/outline-storybook/stories/components/outline-nav.stories.ts b/packages/outline-storybook/stories/components/outline-nav.stories.ts new file mode 100644 index 000000000..463aff89a --- /dev/null +++ b/packages/outline-storybook/stories/components/outline-nav.stories.ts @@ -0,0 +1,89 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { html, TemplateResult } from 'lit'; +import '@phase2/outline-nav-menu'; +import testData, { + jsonData, +} from '../../../outline-nav-menu/src/nav-test-data'; +export default { + title: 'Navigation/Outline Main Menu', + component: 'outline-nav', + argTypes: { + menuEndpoint: { + description: + 'JSON:API Menu Items module generated endpoint.
    https://www.drupal.org/project/jsonapi_menu_items
    An example of the data can be found in /outline-nav-menu/src/nav-test-data.ts', + name: 'JSON API Endpoint', + control: { + type: 'text', + }, + }, + apiExampleDATA: { + table: { + disable: true, + }, + }, + items: { + table: { + disable: true, + }, + }, + }, + args: { + items: testData, + apiExampleDATA: jsonData, + menuEndpoint: 'http://demo.docksal.site/jsonapi/menu_items/main', + }, +}; +// use chrome extension to allow fetch test +// https://chrome.google.com/webstore/detail/cross-domain-cors/mjhpgnbimicffchbodmgfnemoghjakai/related + +const MenuDataTemplate = ({ items }): TemplateResult => + html` + + + `; + +const MenuAPITemplate = ({ menuEndpoint }): TemplateResult => + html` + + + `; + +const SlotsTemplate = (): TemplateResult => + html` + + + + Sub Link 1/1/1 + + + Sub Link 1/2/1 + + + + + Sub Link 2/1/1 + + + + + Sub Link 3/1/1 + + + Sub Link 3/2/1 + + + Sub Link 3/3 + + Main Link 4 + + `; + +export const NavWithPreformattedData = MenuDataTemplate.bind({}); + +export const NavWithDynamicFetchURL = MenuAPITemplate.bind({}); + +export const NavWithSlots = SlotsTemplate.bind({}); diff --git a/yarn.lock b/yarn.lock index 3b970a7f1..137a83516 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4120,11 +4120,23 @@ "@types/koa-compose" "*" "@types/node" "*" +"@types/lodash-es@^4.17.7": + version "4.17.7" + resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.7.tgz#22edcae9f44aff08546e71db8925f05b33c7cc40" + integrity sha512-z0ptr6UI10VlU6l5MYhGwS4mC8DZyYer2mCoyysZtSF7p26zOX8UpbrV0YpNYLGS8K4PUFIyEr62IMFFjveSiQ== + dependencies: + "@types/lodash" "*" + "@types/lodash@*", "@types/lodash@^4.14.167": version "4.14.187" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.187.tgz#122ff0a7192115b4c1a19444ab4482caa77e2c9d" integrity sha512-MrO/xLXCaUgZy3y96C/iOsaIqZSeupyTImKClHunL5GrmaiII2VwvWmLBu2hwa0Kp0sV19CsyjtrTc/Fx8rg/A== +"@types/lodash@^4.14.192": + version "4.14.192" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.192.tgz#5790406361a2852d332d41635d927f1600811285" + integrity sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A== + "@types/mdast@^3.0.0": version "3.0.10" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af" @@ -11340,6 +11352,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"