diff --git a/README.md b/README.md index 04fc902..d3a45ed 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,21 @@ React select component library It's a react component made for Openclassroom hr-net project +# Summary + +- [Getting Started](#getting-started) +- [Dependencies](#dependencies) +- [Installing](#installing) +- [Executing program](#executing-program) +- [Props](#props) +- [Data Structure](#data-structure) +- [Category Object](#category-object) +- [Item Object](#item-object) +- [Example usage](#example-usage) +--- +- [Authors](#authors) +- [Version History](#version-history) + ## Getting Started ### Dependencies @@ -25,10 +40,10 @@ npm install voidsplit-select-component ``` import { SelectMenu } from 'voidsplit_select_component'; - + ``` * Data Example: -``` +```javascript const data = [ { type: "category", @@ -36,23 +51,113 @@ const data = [ categoryContent: [ { type: "item", + id: 0, display: "Item Display Value", disabled: true }, { type: "item", + id: 1, display: "2nd Item Display Value" } ] }, { type: "item", + id: 2, abbreviation: "Item Abreviation", display: "3rd Item Display Value" } ] ``` +### Props +The `SelectMenu` component accepts the following props: + +- `innerRef` (function): A reference callback to access the DOM element of the input. +- `data` (array): An array of objects representing the data to be displayed in the selection menu. +- `id` (string): A unique identifier for the selection menu component. +- `label` (string): The label for the selection menu. + +## Data Structure + +The data structure for the selection menu component should follow a specific format to ensure proper rendering. The data array should consist of objects representing either categories or individual items. Here's how the data should be structured: + +Each object in the `data` array represents either a category or an item: + +### Category Object + +A category object should have the following properties: + +- `type` *(string)*: Set to `"category"` to indicate that this object represents a category. +- `categoryName` *(string)*: The name of the category. +- `categoryContent` *(array)*: An array containing the items within the category. Each item should be an object with its own properties. + +### Item Object + +An item object should have the following properties: + +- `type` *(string)*: Set to `"item"` to indicate that this object represents an item. +- `display` *(string)*: The display text for the item. +- `abbreviation` *(string, optional)*: Abbreviated text for the item (optional). +- `id` *(number)*: A unique identifier for the item. +- `disabled` *(boolean, optional)*: Whether the item is disabled (optional). + +Here's an example of a properly structured `data` array: + +```javascript +const componentData = [ + { + type: "category", + categoryName: "Category 0", + categoryContent: [ + { + type: "item", + display: "Item Display Value", + abbreviation: "Item Abbreviation", + id: 0 + }, + { + type: "item", + display: "2nd Item Display Value", + id: 1 + } + ] + }, + { + type: "item", + display: "3rd Item Display Value", + abbreviation: "Item Abbreviation", + id: 2 + }, + // ... more items or categories +]; + +``` + +## Example usage + +Here's an example of how to use the component: +```javascript +import React, { useRef } from 'react'; +import { SelectMenu } from 'voidsplit_select_component'; + +const App = () => { + const componentRef = useRef(null); + + const componentData = [ + // ... your data array + ]; + + return ( +
+ +
+ ); +} + +export default App; +``` ## Authors Contributors names and contact info @@ -61,6 +166,12 @@ Contributors names and contact info ## Version History +* 0.3.1 + * Component Optimisations + * Adding jsdoc documentation + * Improved readMe documentation +* 0.3.0 + * Total component redesign * 0.2.13 * SEO optimization on npm * 0.2.12 diff --git a/package-lock.json b/package-lock.json index 6ceade5..d8fb149 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "voidsplit_select_component", - "version": "0.2.6", + "version": "0.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "voidsplit_select_component", - "version": "0.2.6", + "version": "0.3.0", "dependencies": { "@emotion/react": "^11.10.8", "@emotion/styled": "^11.10.8", + "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", "tailwindcss": "^3.3.2", @@ -4165,7 +4166,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -8368,7 +8368,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", diff --git a/package.json b/package.json index e99942d..8ade111 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,15 @@ { "name": "voidsplit_select_component", - "version": "0.2.13", - "keywords": ["react", "react component", "select", "select menu", "vite", "select component", "openclassroom"], + "version": "0.3.0", + "keywords": [ + "react", + "react component", + "select", + "select menu", + "vite", + "select component", + "openclassroom" + ], "type": "module", "main": "./dist/react-vite-library.umd.cjs", "module": "./dist/react-vite-library.es.js", @@ -27,6 +35,7 @@ "dependencies": { "@emotion/react": "^11.10.8", "@emotion/styled": "^11.10.8", + "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", "tailwindcss": "^3.3.2", diff --git a/src/components/SelectMenu/Category.jsx b/src/components/SelectMenu/Category.jsx new file mode 100644 index 0000000..0d9e8a4 --- /dev/null +++ b/src/components/SelectMenu/Category.jsx @@ -0,0 +1,42 @@ +// import PropTypes from 'prop-types'; +/* eslint-disable react/prop-types */ + +import Item from "./Item"; + +/** + * Represents a category within a selection menu. + * + * @component + * @param {Object} props - The component properties. + * @param {string} props.name - The name of the category. + * @param {Array} props.list - The list of items within the category. + * @param {function} props.itemCallback - The callback function triggered when an item within the category is selected. + * @param {number} props.selected - The ID of the currently selected item. + * @param {function} props.toggleOpen - Callback to toggle the open state of the menu. + * @returns {JSX.Element} A component representing a category in the selection menu. + */ +export default function Category({name, list, itemCallback, selected, toggleOpen}) { + return ( +
+
{name}
+ {/* Loop through the list to render items within the category */} + {list.map((el, index) => { + switch(el.type) { + case "item": + return + default: + return false + } + })} +
+ ); +} + + +// Category.propTypes = { +// name: PropTypes.string, +// list: PropTypes.array, +// itemCallback: PropTypes.func, +// selected: PropTypes.bool, +// toggleOpen: PropTypes.func +// } \ No newline at end of file diff --git a/src/components/SelectMenu/Item.jsx b/src/components/SelectMenu/Item.jsx new file mode 100644 index 0000000..279d500 --- /dev/null +++ b/src/components/SelectMenu/Item.jsx @@ -0,0 +1,50 @@ +// import PropTypes from 'prop-types'; +/* eslint-disable react/prop-types */ + +/** + * Represents an item within a selection menu. + * + * @component + * @param {Object} props - The component properties. + * @param {function} props.action - The callback function triggered when the item is selected. + * @param {string} props.display - The display text for the item. + * @param {string} props.abbreviation - The abbreviated text for the item (optional). + * @param {number} props.id - The unique identifier for the item. + * @param {boolean} props.selected - Indicates whether the item is currently selected. + * @param {function} props.toggleOpen - Callback to toggle the open state of the menu. + * @returns {JSX.Element} A button representing the selectable item. + */ +export default function Item({action, display, abbreviation, id, selected, toggleOpen}) { + // Determine the displayed text based on abbreviation or display value + let displayed = abbreviation ? abbreviation : display ? display : "error" + return ( + + ); +} + + +// Item.propTypes = { +// action: PropTypes.func, +// display: PropTypes.string, +// abbreviation: PropTypes.string, +// id: PropTypes.number, +// selected: PropTypes.bool, +// toggleOpen: PropTypes.func +// } \ No newline at end of file diff --git a/src/components/SelectMenu/SelectMenu.jsx b/src/components/SelectMenu/SelectMenu.jsx index 39a147e..914555e 100644 --- a/src/components/SelectMenu/SelectMenu.jsx +++ b/src/components/SelectMenu/SelectMenu.jsx @@ -1,110 +1,93 @@ -/* eslint-disable react/prop-types */ // import PropTypes from 'prop-types'; +/* eslint-disable react/prop-types */ +import Category from './Category'; +import Item from './Item'; -import { useEffect, useState } from "react" - -import './selectMenu.css' +import { useState } from 'react'; +import './styles/selectMenu.css' -function SelectMenu({data, selectedItem, innerRef, id}) { - let displayItem; - selectedItem ? displayItem = data.filter(e => e.name === selectedItem)[0] : displayItem = data[0].type === "category" ? data[0].categoryContent.filter(el => el.disabled !== true)[0] : data[0] - let displayList = [...data] +/** + * Interactive selection menu component. + * + * @component + * @param {Object} props - The component properties. + * @param {function} props.innerRef - Reference to the callback function for accessing the DOM element. + * @param {string} props.id - Unique ID for the selection menu element. + * @param {Array} props.data - The data to display in the selection menu. + * @param {string} props.label - The label for the selection menu. + * @returns {JSX.Element} React element representing the selection menu. + */ +export function SelectMenu({innerRef, id, data, label}) { + // State to manage whether the menu is opened or not + const [isOpened, toggleOpen] = useState(false) - const [selected, changeSelected] = useState(displayItem) - const [isOpen, toggleOpen] = useState(false) - const [inputValue, setInputValue] = useState(""); + // Recursive function to find an item by ID within the data hierarchy + function findItemById(list, idToFind) { + for (const item of list) { + if (item.id === idToFind) { + return item; + } + if (item.type === "category" && item.categoryContent) { + const foundItem = findItemById(item.categoryContent, idToFind); + if (foundItem) { + return foundItem; + } + } + } + return null; + } + // Find the initial selected item using the findItemById function + const foundItem = findItemById(data, 0); - let searchAutorisation = false + // State to manage the currently selected item + const [selected, ChangeSelected] = useState(foundItem) - const handleOpenClick = () => { - toggleOpen(!isOpen) - } - const callbackFunction = (props) => { - changeSelected(props) - handleOpenClick() + // Callback function to handle item selection + const handleCallback = (id) => { + toggleOpen(false) + ChangeSelected(findItemById(data, id)) } - useEffect(() => { - setInputValue(selected.abbreviation ? selected.abbreviation : selected.display); - }, [selected]) - + return ( -
-
-
+
+ +
setInputValue(e.target.value)} - disabled={!searchAutorisation} - ref={innerRef} - id={`${id ? id : Date.now() + Math.random()}`} - /> -
-
- {displayList.map((el, index) => { - switch(el.type) { - case "item": - return - case "category": - return - default: - return false - } - })} -
-
-
- ); -} + type="text" + id={id} + ref={innerRef} + value={selected.display} -function SelectCategory({element, actualDisplay, callback}) { - return ( -
-

{element.categoryName}

- {element.categoryContent.map((el, index) => { - return - })} -
- ); -} -function SelectItem({ element, isSelected, callback }) { - const handleClick = () => { - callback(element); - }; - - return ( -
- {element.display} -
- - - -
+ onChange={() => null} + onFocus={() => { + toggleOpen(true) + }} + onBlur={() => { + toggleOpen(false) + }} + /> +
+
+ {data.map((el, index) => { + switch(el.type) { + case "item": + return + case "category": + return el.type === "category").findIndex(item => item.categoryName === el.categoryName)}/> + default: + return false + } + })} +
+ ); } -export {SelectMenu}; - // SelectMenu.propTypes = { -// data: PropTypes.object, -// selectedItem: PropTypes.array, -// innerRef: PropTypes.func -// } -// -// -// SelectCategory.propTypes = { -// element: PropTypes.object, -// actualDisplay: PropTypes.array, -// callback: PropTypes.func -// } -// -// -// SelectItem.propTypes = { -// element: PropTypes.object, -// isSelected: PropTypes.bool, -// callback: PropTypes.func -// } -// -// \ No newline at end of file +// innerRef: PropTypes.func, +// id: PropTypes.string, +// data: PropTypes.array, +// label: PropTypes.string, +// } \ No newline at end of file diff --git a/src/components/SelectMenu/selectMenu.css b/src/components/SelectMenu/selectMenu.css deleted file mode 100644 index d96f7cc..0000000 --- a/src/components/SelectMenu/selectMenu.css +++ /dev/null @@ -1,111 +0,0 @@ - -.select-menu-wrapper { - --display-rows: 8; - display: flex; - flex-direction: column; -} -.select-menu-wrapper .select-menu { - /* background-color: red; */ - position: relative; - display: flex; - flex-direction: column; -} -.select-menu-wrapper .display { - /* background-color: lime; */ - height: 40px; - - border: 1px solid #d9dfe8; - - display: flex; -} -.select-menu-wrapper .display input { - border: none; - width: 100%; - - padding: 0 15px 0 25px; - - cursor: pointer; - border: none !important; -} -.select-menu-wrapper .display input[disabled] { - user-select: none; -} -.select-menu-wrapper .display input:focus { - outline: none; -} - -.select-menu-wrapper .select-menu.open .select-list { - --rows: var(--display-rows) -} -.select-menu-wrapper .select-list { - /* background-color: pink; */ - width: 100%; - position: absolute; - top: 100%; - display: flex; - flex-direction: column; - justify-content: start; - background-color: #d9dfe8; - overflow: auto; - max-height: calc(var(--rows) * 40px); - transition: max-height .2s; - --rows: 0; - z-index: 25; -} - -.select-menu-wrapper .select-list .category { - display: flex; - flex-direction: column; -} -.select-menu-wrapper .select-list .category-name { - display: flex; - align-items: center; - height: 40px; - padding: 0 25px; - font-weight: 600; - letter-spacing: 0.3px; - cursor: default; -} - -.select-menu-wrapper .category .item { - background-color: rgba(255, 255, 255, 0.2); -} -.select-menu .item { - display: flex; - align-items: center; - min-height: 40px; - height: 40px ; - padding: 0 25px; -} -.select-menu .item.selected { - background-color: #A5C7F3; - cursor: pointer; -} -.select-menu .item.selected .check-indicator { - background: #2878E2; - position: relative; - height: 20px; - width: 20px; - margin: 0 0 0 auto; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; -} -.select-menu .item.selected .check-indicator svg { - fill: #fff; - font-size: 0.8rem; -} -.select-menu .item:not(.selected) .check-indicator { - display: none; -} -.select-menu .item:not(.disabled) { - cursor: pointer; -} -.select-menu .item.disabled { - color: #bbb; - cursor: default; -} -.select-menu .item:not(.disabled, .selected):hover { - background-color: #e8edf3; -} \ No newline at end of file diff --git a/src/components/SelectMenu/styles/selectMenu.css b/src/components/SelectMenu/styles/selectMenu.css new file mode 100644 index 0000000..b85b8d5 --- /dev/null +++ b/src/components/SelectMenu/styles/selectMenu.css @@ -0,0 +1,118 @@ +.select-menu:hover, +.select-menu:focus { + --border-color: rgba(0, 0, 0, 0.7) +} +.select-menu:focus-within { + --border-color: rgb(25, 118, 210); +} +.select-menu:focus-within { + border-width: 2px; +} +.select-menu { + --border-color: rgba(0, 0, 0, 0.23); + + position: relative; + height: 56px; + border-radius: 4px; + border: 1px solid var(--border-color); +} +.select-menu .display { + height: 100%; + width: 100%; + border-radius: 4px; + overflow: hidden; +} +.select-menu input { + width: 100%; + height: 100%; + border: none; + padding: 0 15px; + font-size: 16px; + font: inherit; + letter-spacing: inherit; + font-family: Roboto, Helvetica, Arial, sans-serif; +} +.select-menu input:focus { + outline: none; +} +.select-menu:focus-within .label { + color: rgb(25, 118, 210); +} +.select-menu .label { + background-color: #fff; + padding: 0 5px; + color: rgba(0, 0, 0, 0.8); + font-size: 13px; + font-weight: 400; + position: absolute; + font-family: "Roboto","Helvetica","Arial",sans-serif; + left: 10px; + top: calc(-13px / 2); +} +.select-menu .list::before { + position: absolute; + display: block; + content: ''; + top: 0; + left: 0; + width: 100%; + height: 1px; + background-color: var(--border-color); +} +.select-menu:focus-within .list { + border-width: 2px; + width: calc(100% + 4px); + left: -2px; +} +.select-menu .list.open { + max-height: 250px; + border-bottom: 1px solid var(--border-color); +} +.select-menu .list { + overflow: hidden; + z-index: 2; + background-color: #fff; + position: absolute; + width: calc(100% + 2px); + left: -1px; + top: calc(100% - 3px); + display: flex; + flex-direction: column; + + border-radius: 0 0 4px 5px; + border-left: 1px solid var(--border-color); + border-right: 1px solid var(--border-color); + + max-height: 0; + overflow: auto; +} +.select-menu .category { + background-color: #fff; + display: flex; + flex-direction: column; +} +.select-menu .category .category-name { + padding: 0 15px; + height: 25px; + background-color: #e8edf3; + display: flex; + align-items: center; +} +.select-menu .item { + height: 50px; + min-height: 25px; + padding: 0 15px; + display: flex; + align-items: center; + cursor: pointer; + border: none; +} +.select-menu .item:hover, +.select-menu .item:focus { + background-color: #dce2e9; + outline: none; +} +.select-menu .item.selected { + background-color: rgb(25, 118, 210); + color: #fff; +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index e69de29..0000000