From a9cb3f7e0dda2f8b3643fb074bc482f8c71b3adc Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Fri, 29 Nov 2024 22:59:46 +0800 Subject: [PATCH 1/3] feat: add Select component --- packages/components/select/index.scss | 139 ++++++++++++++++++++++++ packages/components/select/index.tsx | 147 ++++++++++++++++++++++++++ packages/index.scss | 1 + src/App.tsx | 2 + src/components/select.tsx | 25 +++++ 5 files changed, 314 insertions(+) create mode 100644 packages/components/select/index.scss create mode 100644 packages/components/select/index.tsx create mode 100644 src/components/select.tsx diff --git a/packages/components/select/index.scss b/packages/components/select/index.scss new file mode 100644 index 0000000..8d27295 --- /dev/null +++ b/packages/components/select/index.scss @@ -0,0 +1,139 @@ +.alley-select { + --alley-select-selector-bg: #fff; + --alley-select-select-affix-padding: 4px; + + box-sizing: border-box; + margin: 0; + padding: 0; + color: var(--alley-color-text); + font-size: var(--alley-font-size); + line-height: var(--alley-line-height); + list-style: none; + font-family: var(--alley-font-family); + position: relative; + display: inline-flex; + height: 32px; + + &-selector { + border: var(--alley-line-width) var(--alley-line-type) + var(--alley-color-border); + background: var(--alley-select-selector-bg); + width: 100%; + height: 100%; + align-items: center; + margin: 0; + padding: 0 calc(var(--alley-padding-l) - 1px); + position: relative; + transition: all var(--alley-motion-duration-mid) + var(--alley-motion-ease-in-out); + box-sizing: border-box; + color: var(--alley-color-text); + font-size: var(--alley-font-size); + line-height: var(--alley-line-height); + list-style: none; + font-family: inherit; + display: flex; + border-radius: var(--alley-border-radius); + flex: 1 1 auto; + } + + &-prefix { + flex: none; + margin-inline-end: var(--alley-select-select-affix-padding); + } + + &-placeholder { + color: var(--alley-color-weak); + } + + &-arrow { + user-select: none; + display: flex; + align-items: center; + color: var(--alley-color-text-quaternary); + font-style: normal; + line-height: 1; + text-align: center; + text-transform: none; + vertical-align: -0.125em; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + position: absolute; + top: 50%; + inset-inline-start: auto; + inset-inline-end: calc(var(--alley-padding-l) - 1px); + height: var(--alley-font-size-icon); + margin-top: calc(var(--alley-font-size-icon) * -1 / 2); + font-size: var(--alley-font-size-icon); + pointer-events: none; + transition: opacity var(--alley-motion-duration-slow) ease; + } + + &-item { + &-option { + --alley-select-option-height: 32px; + --alley-select-option-padding: 5px 12px; + --alley-select-option-font-size: 14px; + --alley-select-option-line-height: 1.5714285714285714; + --alley-select-option-selected-font-weight: 600; + --alley-select-option-selected-color: rgba(0, 0, 0, 0.88); + --alley-select-option-selected-bg: var(--alley-slate-blue-1); + --alley-select-option-hover-bg: rgba(0, 0, 0, 0.04); + + position: relative; + display: block; + min-height: var(--alley-select-option-height); + padding: var(--alley-select-option-padding); + color: var(--alley-color-text); + font-weight: normal; + font-size: var(--alley-select-option-font-size); + line-height: var(--alley-select-option-line-height); + box-sizing: border-box; + transition: background var(--alley-motion-duration-slow) ease; + border-radius: var(--alley-border-radius-sm); + + &-selected:not(.alley-select-item-option-disabled) { + color: var(--alley-select-option-selected-color); + font-weight: var(--alley-select-option-selected-font-weight); + background-color: var(--alley-select-option-selected-bg); + } + + &:not(.alley-select-item-option-selected):hover { + background-color: var(--alley-select-option-hover-bg); + } + } + } +} + +.alley-select-dropdown { + box-sizing: border-box; + margin: 0; + padding: var(--alley-padding-xxs); + color: var(--alley-color-text); + font-size: var(--alley-font-size); + line-height: var(--alley-line-height); + list-style: none; + font-family: var(--alley-font-family); + position: fixed; + top: 0; + z-index: var(--alley-select-z-index-popup); + overflow: hidden; + font-variant: initial; + background-color: var(--alley-color-bg-elevated); + border-radius: var(--alley-border-radius-lg); + outline: none; + box-shadow: var(--alley-box-shadow-secondary); +} + +.dark .alley-select { + --alley-select-selector-bg: #141414; + + &-item { + &-option { + --alley-select-option-selected-color: rgba(255, 255, 255, 0.85); + --alley-select-option-selected-bg: var(--alley-slate-blue-2); + --alley-select-option-hover-bg: rgba(255, 255, 255, 0.08); + } + } +} diff --git a/packages/components/select/index.tsx b/packages/components/select/index.tsx new file mode 100644 index 0000000..70a5053 --- /dev/null +++ b/packages/components/select/index.tsx @@ -0,0 +1,147 @@ +import { + children, + createEffect, + createSignal, + For, + mergeProps, + onCleanup, + Show, + type JSX, +} from "solid-js"; +import "./index.scss"; +import type { BaseNoChildrenComponentProps, SizeType } from "~/interface"; +import { classList } from "~/utils"; +import { AiOutlineDown, AiOutlineUp } from "solid-icons/ai"; +import { Portal } from "solid-js/web"; + +interface SelectOption { + label: string; + value: string; +} + +interface SelectProps extends BaseNoChildrenComponentProps { + options: SelectOption[]; + placeholder?: string; + defaultValue?: string; + position?: "top" | "bottom"; + size?: SizeType; + prefix?: JSX.Element; + suffixIcon?: JSX.Element; + onChange?: (value: string) => void; +} + +const baseClass = "alley-select"; + +const Select = (props: SelectProps) => { + let ref: HTMLDivElement | undefined; + + const merged = mergeProps({ position: "bottom" }, props); + + const [selectedValue, setSelectedValue] = createSignal( + merged.defaultValue || "", + ); + const [isOpen, setIsOpen] = createSignal(false); + const [dropdownStyle, setDropdownStyle] = createSignal(merged.style); + + const updateStyle = () => { + const rect = ref!.getBoundingClientRect(); + setDropdownStyle((prev) => ({ + ...(prev ?? {}), + top: `${rect.top + rect.height + 4}px`, + left: `${rect.left}px`, + })); + }; + + createEffect(() => { + if (isOpen()) { + updateStyle(); + window.addEventListener("resize", updateStyle); + window.addEventListener("scroll", updateStyle); + } else { + window.removeEventListener("resize", updateStyle); + window.removeEventListener("scroll", updateStyle); + } + }); + + onCleanup(() => { + window.removeEventListener("resize", updateStyle); + window.removeEventListener("scroll", updateStyle); + }); + + const classes = () => + classList({ + base: baseClass, + others: { + [`${baseClass}-${merged.size}`]: !!merged.size, + [`${baseClass}-open`]: isOpen(), + }, + }); + + const toggleDropdown = () => setIsOpen(!isOpen()); + const selectOption = (option: SelectOption) => { + setSelectedValue(option.value); + merged.onChange?.(option.value); + setIsOpen(false); + }; + + const selectedLabel = children(() => { + const selectedOption = merged.options.find( + (option) => option.value === selectedValue(), + ); + return selectedOption ? ( + {selectedOption.label} + ) : ( + {merged.placeholder} || "" + ); + }); + + return ( +
+
+ +
{merged.prefix}
+
+ {selectedLabel()} +
+ + + + }> + + + + + + + +
+
    + + {(option) => ( +
  • selectOption(option)} + > + {option.label} +
  • + )} +
    +
+
+
+
+
+ ); +}; + +export default Select; diff --git a/packages/index.scss b/packages/index.scss index d058074..1b771a7 100644 --- a/packages/index.scss +++ b/packages/index.scss @@ -337,6 +337,7 @@ input[disabled] { --alley-color-text-reverse: #1a1a1a; --alley-color-text-disabled: #6a6a6a; --alley-color-text-description: rgba(255, 255, 255, 0.45); + --alley-color-text-quaternary: rgba(255, 255, 255, 0.25); --alley-color-shadow: #000; diff --git a/src/App.tsx b/src/App.tsx index e4b7b7a..a934be3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,7 @@ const children = [ lazy(() => import("./components/switchs.tsx")), lazy(() => import("./components/dialogs.tsx")), lazy(() => import("./components/tags.tsx")), + lazy(() => import("./components/select.tsx")), ]; const LazyMenu = lazy(() => import("~/components/menu")); @@ -36,6 +37,7 @@ const menus = [ "开关", "对话框", "标签", + "选择器", ]; const App = () => { diff --git a/src/components/select.tsx b/src/components/select.tsx new file mode 100644 index 0000000..943e0cb --- /dev/null +++ b/src/components/select.tsx @@ -0,0 +1,25 @@ +import { AiFillApi } from "solid-icons/ai"; +import Select from "~/components/select"; + +const SelectDemo = () => { + const options = [ + { label: "Option 1", value: "1" }, + { label: "Option 2", value: "2" }, + { label: "Option 3", value: "3" }, + ]; + + return ( +