Skip to content

Commit

Permalink
feat(Select): add Select component
Browse files Browse the repository at this point in the history
chore: use own SelectButton
  • Loading branch information
zettca committed Feb 20, 2024
1 parent 092a81a commit b6e6640
Show file tree
Hide file tree
Showing 14 changed files with 925 additions and 2 deletions.
33 changes: 32 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"@hitachivantara/uikit-react-shared": "^5.1.28",
"@hitachivantara/uikit-styles": "^5.19.0",
"@internationalized/date": "^3.2.0",
"@mui/base": "^5.0.0-beta.4",
"@mui/base": "^5.0.0-beta.34",
"@popperjs/core": "^2.11.8",
"@react-aria/datepicker": "^3.9.0",
"@react-stately/datepicker": "^3.9.0",
Expand Down
72 changes: 72 additions & 0 deletions packages/core/src/Select/Option.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useRef } from "react";
import { useOption } from "@mui/base/useOption";
import { OptionOwnProps } from "@mui/base/Option";
import { useForkRef } from "@mui/material/utils";

import { HvListItem, HvListItemProps } from "../ListContainer";
import { useDefaultProps } from "../hooks/useDefaultProps";
import { fixedForwardRef } from "../types/generic";
import { ExtractNames, createClasses } from "../utils/classes";
import { outlineStyles } from "../utils/focusUtils";

const { staticClasses, useClasses } = createClasses("HvOption", {
root: {},
highlighted: {
...outlineStyles,
},
});

export { staticClasses as optionClasses };

export type HvOptionClasses = ExtractNames<typeof useClasses>;

export interface HvOptionProps<OptionValue extends {}>
extends Omit<HvListItemProps, "value" | "disabled">,
Pick<OptionOwnProps<OptionValue>, "disabled" | "label" | "value"> {
classes?: HvOptionClasses;
}

export const HvOption = fixedForwardRef(function HvOption<
OptionValue extends {}
>(props: HvOptionProps<OptionValue>, ref: React.Ref<HTMLLIElement>) {
const {
classes: classesProp,
className,
disabled = false,
label,
value,
children,
...others
} = useDefaultProps("HvOption", props);
const { classes, cx } = useClasses(classesProp);

const optionRef = useRef<HTMLElement>(null);
const rootRef = useForkRef(optionRef, ref);

const computedLabel =
label ??
(typeof children === "string"
? children
: optionRef.current?.textContent?.trim());

const { getRootProps, selected, highlighted } = useOption({
disabled,
label: computedLabel,
rootRef,
value,
});

return (
<HvListItem
ref={ref}
selected={selected}
className={cx(classes.root, className, {
[classes.highlighted]: highlighted,
})}
{...getRootProps()}
{...others}
>
{children}
</HvListItem>
);
});
40 changes: 40 additions & 0 deletions packages/core/src/Select/OptionGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { forwardRef } from "react";
import { OptionGroup, OptionGroupProps } from "@mui/base/OptionGroup";
import { theme } from "@hitachivantara/uikit-styles";

import { ExtractNames, createClasses } from "../utils/classes";
import { useDefaultProps } from "../hooks/useDefaultProps";

const { staticClasses, useClasses } = createClasses("HvOptionGroup", {
root: {
listStyle: "none",
...theme.typography.label,
},
});

export { staticClasses as optionGroupClasses };

export type HvOptionGroupClasses = ExtractNames<typeof useClasses>;

export interface HvOptionGroupProps extends OptionGroupProps {
classes?: HvOptionGroupClasses;
}

export const HvOptionGroup = forwardRef<HTMLLIElement, HvOptionGroupProps>(
(props, ref) => {
const {
className,
classes: classesProp,
...others
} = useDefaultProps("HvOptionGroup", props);
const { classes, cx } = useClasses(classesProp);

return (
<OptionGroup
ref={ref}
className={cx(classes.root, className)}
{...others}
/>
);
}
);
123 changes: 123 additions & 0 deletions packages/core/src/Select/Select.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Decorator, Meta, StoryObj } from "@storybook/react";
import {
HvSelect,
HvSelectProps,
HvOption,
HvOptionGroup,
} from "@hitachivantara/uikit-react-core";

import FormStory from "./stories/Form";
import FormStoryRaw from "./stories/Form?raw";
import ControlledStory from "./stories/Controlled";
import ControlledStoryRaw from "./stories/Controlled?raw";

const decorator: Decorator = (Story) => (
<div className="w-[300px] min-h-[300px]">{Story()}</div>
);

export default {
title: "Components/Select",
component: HvSelect,
// @ts-expect-error https://github.com/storybookjs/storybook/issues/20782
subcomponents: { HvOption, HvOptionGroup },
} satisfies Meta<typeof HvSelect>;

export const Main: StoryObj<HvSelectProps<{}, false>> = {
args: {
multiple: false,
},
argTypes: {},
decorators: [decorator],
render: (args) => {
return (
<HvSelect
required
name="country"
label="Country"
description="Select your favorite country"
placeholder="Select country"
onChange={(evt, val) => console.log(val)}
{...args}
>
<HvOptionGroup label="America">
<HvOption value="ar">Argentina</HvOption>
<HvOption value="us">United States</HvOption>
</HvOptionGroup>
<HvOptionGroup label="Europe">
<HvOption value="bg">Belgium</HvOption>
<HvOption value="pt">Portugal</HvOption>
<HvOption value="pl">Poland</HvOption>
<HvOption value="sp">Spain</HvOption>
</HvOptionGroup>
</HvSelect>
);
},
};

export const Variants: StoryObj<HvSelectProps<{}, false>> = {
parameters: {
docs: {
description: {
story: "Selects in their various form state variants.",
},
},
},
decorators: [
(Story) => (
<div className="flex flex-wrap gap-sm [&>*]:w-[200px]">{Story()}</div>
),
],
render: () => {
return (
<>
<HvSelect required label="Required">
<HvOption value="op">Option</HvOption>
</HvSelect>
<HvSelect disabled label="Disabled">
<HvOption value="op">Option</HvOption>
</HvSelect>
<HvSelect readOnly label="Read-only">
<HvOption value="op">Option</HvOption>
</HvSelect>
<HvSelect
status="invalid"
label="Invalid"
statusMessage="This is always invalid"
>
<HvOption value="op">Option</HvOption>
</HvSelect>
</>
);
},
};

export const Form: StoryObj<HvSelectProps<{}, false>> = {
parameters: {
docs: {
source: { code: FormStoryRaw },
description: {
story:
"To integrate `HvSelect` in a form, make sure you're giving it a `name`. <br />\
The value result will be the selected option's `value`, or a JSON of the selected values when multi-select is enabled. The value can be customized via the `getSerializedValue` prop.",
},
},
},
decorators: [decorator],
render: () => <FormStory />,
};

export const Controlled: StoryObj<HvSelectProps<{}, false>> = {
parameters: {
docs: {
source: { code: ControlledStoryRaw },
description: {
story:
"The value and open states of `HvSelect` can be controlled by using the `value`/`onChange` and `open`/`onOpenChange` props respectively.",
},
},
},
decorators: [
(Story) => <div className="flex gap-sm min-h-[300px]">{Story()}</div>,
],
render: () => <ControlledStory />,
};
42 changes: 42 additions & 0 deletions packages/core/src/Select/Select.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { theme } from "@hitachivantara/uikit-styles";

import { createClasses } from "../utils/classes";

export const { staticClasses, useClasses } = createClasses("HvSelect", {
root: {
position: "relative",
"&$disabled,&$readOnly": {
pointerEvents: "none",
},
},
disabled: {},
readOnly: {},
invalid: {
border: `1px solid ${theme.colors.negative}`,
},
labelContainer: {
display: "flex",
alignItems: "flex-start",
},
label: {
display: "block",
paddingBottom: 6,
},
description: {},
select: {},
popper: {
zIndex: theme.zIndices.popover,
},
panel: {
border: `1px solid ${theme.colors.secondary}`,
marginTop: -1,
marginBottom: -1,
},
panelOpenedUp: {
borderRadius: `${theme.radii.base} ${theme.radii.base} 0 0`,
},
panelOpenedDown: {
borderRadius: `0 0 ${theme.radii.base} ${theme.radii.base}`,
},
error: {},
});

0 comments on commit b6e6640

Please sign in to comment.