diff --git a/apps/scandic-web/components/Blocks/Accordion/index.tsx b/apps/scandic-web/components/Blocks/Accordion/index.tsx index 3906f2711..a5161aa4a 100644 --- a/apps/scandic-web/components/Blocks/Accordion/index.tsx +++ b/apps/scandic-web/components/Blocks/Accordion/index.tsx @@ -6,10 +6,10 @@ import { useState } from "react" import Accordion from "@scandic-hotels/design-system/Accordion" import AccordionItem from "@scandic-hotels/design-system/Accordion/AccordionItem" import { JsonToHtml } from "@scandic-hotels/design-system/JsonToHtml" +import { ShowMoreButton } from "@scandic-hotels/design-system/ShowMoreButton" import { Section } from "@/components/Section" import { SectionHeader } from "@/components/Section/Header" -import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton" import styles from "./accordion.module.css" diff --git a/apps/scandic-web/components/Blocks/Table/index.tsx b/apps/scandic-web/components/Blocks/Table/index.tsx index 4d1f704a5..8ecc12dd9 100644 --- a/apps/scandic-web/components/Blocks/Table/index.tsx +++ b/apps/scandic-web/components/Blocks/Table/index.tsx @@ -8,11 +8,11 @@ import { } from "@tanstack/react-table" import { useState } from "react" +import { ShowMoreButton } from "@scandic-hotels/design-system/ShowMoreButton" import Table from "@scandic-hotels/design-system/Table" import { Section } from "@/components/Section" import { SectionHeader } from "@/components/Section/Header" -import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton" import styles from "./table.module.css" diff --git a/apps/scandic-web/components/ContentType/HotelPage/Rooms/index.tsx b/apps/scandic-web/components/ContentType/HotelPage/Rooms/index.tsx index c0f3db4fe..61836fe05 100644 --- a/apps/scandic-web/components/ContentType/HotelPage/Rooms/index.tsx +++ b/apps/scandic-web/components/ContentType/HotelPage/Rooms/index.tsx @@ -3,9 +3,10 @@ import { cx } from "class-variance-authority" import { useMemo, useRef, useState } from "react" +import { ShowMoreButton } from "@scandic-hotels/design-system/ShowMoreButton" + import { Section } from "@/components/Section" import { SectionHeader } from "@/components/Section/Header" -import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton" import { RoomCard } from "./RoomCard" diff --git a/apps/scandic-web/components/ContentType/HotelPage/SidePeeks/Room/RoomFacilities/index.tsx b/apps/scandic-web/components/ContentType/HotelPage/SidePeeks/Room/RoomFacilities/index.tsx index 0714b3f80..c62bdb00c 100644 --- a/apps/scandic-web/components/ContentType/HotelPage/SidePeeks/Room/RoomFacilities/index.tsx +++ b/apps/scandic-web/components/ContentType/HotelPage/SidePeeks/Room/RoomFacilities/index.tsx @@ -5,10 +5,9 @@ import { useRef, useState } from "react" import { useIntl } from "react-intl" import { FacilityIcon } from "@scandic-hotels/design-system/Icons/FacilityIcon" +import { ShowMoreButton } from "@scandic-hotels/design-system/ShowMoreButton" import { Typography } from "@scandic-hotels/design-system/Typography" -import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton" - import styles from "./roomFacilities.module.css" import type { Room } from "@scandic-hotels/trpc/types/hotel" diff --git a/apps/scandic-web/components/ContentType/HotelSubpage/MeetingsSubpage/MeetingRooms/MeetingRoomCard/index.tsx b/apps/scandic-web/components/ContentType/HotelSubpage/MeetingsSubpage/MeetingRooms/MeetingRoomCard/index.tsx index 3ceb350dd..105777d00 100644 --- a/apps/scandic-web/components/ContentType/HotelSubpage/MeetingsSubpage/MeetingRooms/MeetingRoomCard/index.tsx +++ b/apps/scandic-web/components/ContentType/HotelSubpage/MeetingsSubpage/MeetingRooms/MeetingRoomCard/index.tsx @@ -4,10 +4,9 @@ import { useIntl } from "react-intl" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import Image from "@scandic-hotels/design-system/Image" +import { ShowMoreButton } from "@scandic-hotels/design-system/ShowMoreButton" import { Typography } from "@scandic-hotels/design-system/Typography" -import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton" - import { translateRoomLighting, translateSeatingType } from "./utils" import styles from "./meetingRoomCard.module.css" diff --git a/apps/scandic-web/components/ContentType/HotelSubpage/MeetingsSubpage/MeetingRooms/index.tsx b/apps/scandic-web/components/ContentType/HotelSubpage/MeetingsSubpage/MeetingRooms/index.tsx index fcda91032..21988f5de 100644 --- a/apps/scandic-web/components/ContentType/HotelSubpage/MeetingsSubpage/MeetingRooms/index.tsx +++ b/apps/scandic-web/components/ContentType/HotelSubpage/MeetingsSubpage/MeetingRooms/index.tsx @@ -2,7 +2,7 @@ import { cx } from "class-variance-authority" import { useRef, useState } from "react" -import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton" +import { ShowMoreButton } from "@scandic-hotels/design-system/ShowMoreButton" import MeetingRoomCard from "./MeetingRoomCard" diff --git a/apps/scandic-web/components/DestinationFilterAndSort/Filter/Checkbox/checkbox.module.css b/apps/scandic-web/components/DestinationFilterAndSort/Filter/Checkbox/checkbox.module.css index 83c6c99ce..6d99e8c01 100644 --- a/apps/scandic-web/components/DestinationFilterAndSort/Filter/Checkbox/checkbox.module.css +++ b/apps/scandic-web/components/DestinationFilterAndSort/Filter/Checkbox/checkbox.module.css @@ -20,12 +20,12 @@ &[data-disabled] { cursor: not-allowed; - + .checkbox { border-color: var(--UI-Input-Controls-Border-Disabled); background-color: var(--UI-Input-Controls-Surface-Disabled); } - } + } } .checkbox { diff --git a/packages/design-system/lib/components/ShowMoreButton/ShowMoreButton.stories.tsx b/packages/design-system/lib/components/ShowMoreButton/ShowMoreButton.stories.tsx new file mode 100644 index 000000000..3a45fb3b5 --- /dev/null +++ b/packages/design-system/lib/components/ShowMoreButton/ShowMoreButton.stories.tsx @@ -0,0 +1,217 @@ +import type { Meta, StoryObj } from "@storybook/nextjs-vite" +import React, { useState } from "react" + +import { ShowMoreButton } from "./ShowMoreButton" + +const meta: Meta = { + title: "Core Components/ShowMoreButton", + component: ShowMoreButton, + argTypes: { + loadMoreData: { + table: { + type: { summary: "function" }, + defaultValue: { summary: "undefined" }, + }, + description: "Callback function to handle button press events.", + }, + showLess: { + control: "boolean", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + }, + description: "When true, shows 'Show less' text and up arrow icon.", + }, + textShowMore: { + control: "text", + table: { + type: { summary: "string" }, + defaultValue: { summary: "undefined (uses i18n)" }, + }, + description: + "Custom text for 'Show more' state. If not provided, uses i18n message.", + }, + textShowLess: { + control: "text", + table: { + type: { summary: "string" }, + defaultValue: { summary: "undefined (uses i18n)" }, + }, + description: + "Custom text for 'Show less' state. If not provided, uses i18n message.", + }, + variant: { + control: "select", + options: ["Primary", "Secondary", "Tertiary", "Text"], + table: { + type: { summary: "Primary | Secondary | Tertiary | Text" }, + defaultValue: { summary: "Text" }, + }, + }, + color: { + control: "select", + options: ["Primary", "Inverted"], + table: { + type: { summary: "Primary | Inverted" }, + defaultValue: { summary: "Primary" }, + }, + }, + size: { + control: "select", + options: ["sm", "md", "lg"], + table: { + type: { summary: "sm | md | lg" }, + defaultValue: { summary: "md" }, + }, + }, + }, +} + +export default meta + +type Story = StoryObj + +// Wrapper component that manages state for interactive stories +function InteractiveShowMoreButton( + props: Omit< + React.ComponentProps, + "showLess" | "loadMoreData" + > & { + initialShowLess?: boolean + } +) { + const [showLess, setShowLess] = useState(props.initialShowLess ?? false) + + const handleLoadMore = () => { + setShowLess((prev) => !prev) + } + + return ( + + ) +} + +export const Default: Story = { + render: (args) => , + args: {}, + play: async ({ canvas, userEvent }) => { + const button = await canvas.findByRole("button") + await userEvent.click(button) + // Button should now show "Show less" + await userEvent.click(button) + // Button should now show "Show more" again + }, +} + +export const ShowLess: Story = { + render: (args) => ( + + ), + args: {}, + play: async ({ canvas, userEvent }) => { + const button = await canvas.findByRole("button") + await userEvent.click(button) + // Button should now show "Show more" + await userEvent.click(button) + // Button should now show "Show less" again + }, +} + +export const CustomText: Story = { + render: (args) => , + args: { + textShowMore: "Load more items", + textShowLess: "Show fewer items", + }, + play: async ({ canvas, userEvent }) => { + const button = await canvas.findByRole("button") + await userEvent.click(button) + // Button should now show "Show fewer items" + await userEvent.click(button) + // Button should now show "Load more items" again + }, +} + +export const PrimaryVariant: Story = { + render: (args) => , + args: { + variant: "Primary", + }, + play: async ({ canvas, userEvent }) => { + const button = await canvas.findByRole("button") + await userEvent.click(button) + await userEvent.click(button) + }, +} + +export const SecondaryVariant: Story = { + render: (args) => , + args: { + variant: "Secondary", + }, + play: async ({ canvas, userEvent }) => { + const button = await canvas.findByRole("button") + await userEvent.click(button) + await userEvent.click(button) + }, +} + +export const SmallSize: Story = { + render: (args) => , + args: { + size: "sm", + }, + play: async ({ canvas, userEvent }) => { + const button = await canvas.findByRole("button") + await userEvent.click(button) + await userEvent.click(button) + }, +} + +export const LargeSize: Story = { + render: (args) => , + args: { + size: "lg", + }, + play: async ({ canvas, userEvent }) => { + const button = await canvas.findByRole("button") + await userEvent.click(button) + await userEvent.click(button) + }, +} + +export const InvertedColor: Story = { + globals: { + backgrounds: { value: "scandicPrimaryDark" }, + }, + render: (args) => , + args: { + color: "Inverted", + }, + play: async ({ canvas, userEvent }) => { + const button = await canvas.findByRole("button") + await userEvent.click(button) + await userEvent.click(button) + }, +} + +export const InvertedShowLess: Story = { + globals: { + backgrounds: { value: "scandicPrimaryDark" }, + }, + render: (args) => ( + + ), + args: { + color: "Inverted", + }, + play: async ({ canvas, userEvent }) => { + const button = await canvas.findByRole("button") + await userEvent.click(button) + await userEvent.click(button) + }, +} diff --git a/packages/design-system/lib/components/ShowMoreButton/ShowMoreButton.test.tsx b/packages/design-system/lib/components/ShowMoreButton/ShowMoreButton.test.tsx new file mode 100644 index 000000000..b141a0652 --- /dev/null +++ b/packages/design-system/lib/components/ShowMoreButton/ShowMoreButton.test.tsx @@ -0,0 +1,116 @@ +import { describe, expect, it, vi, afterEach } from "vitest" +import { render, screen, cleanup, fireEvent } from "@testing-library/react" +import { IntlProvider } from "react-intl" + +import { ShowMoreButton } from "./ShowMoreButton" + +afterEach(() => { + cleanup() +}) + +const renderShowMoreButton = ( + props: React.ComponentProps +) => { + return render( + + + + ) +} + +describe("ShowMoreButton", () => { + describe("props", () => { + it("renders with default props", () => { + const loadMoreData = vi.fn() + renderShowMoreButton({ loadMoreData }) + expect(screen.getByRole("button")).toBeTruthy() + }) + + it("displays 'Show more' text by default", () => { + const loadMoreData = vi.fn() + renderShowMoreButton({ loadMoreData }) + expect(screen.getByText("Show more")).toBeTruthy() + }) + + it("displays 'Show less' text when showLess is true", () => { + const loadMoreData = vi.fn() + renderShowMoreButton({ loadMoreData, showLess: true }) + expect(screen.getByText("Show less")).toBeTruthy() + }) + + it("displays custom 'Show more' text when provided", () => { + const loadMoreData = vi.fn() + renderShowMoreButton({ + loadMoreData, + textShowMore: "Load more items", + }) + expect(screen.getByText("Load more items")).toBeTruthy() + }) + + it("displays custom 'Show less' text when provided", () => { + const loadMoreData = vi.fn() + renderShowMoreButton({ + loadMoreData, + showLess: true, + textShowLess: "Show fewer items", + }) + expect(screen.getByText("Show fewer items")).toBeTruthy() + }) + + it("calls loadMoreData when clicked", () => { + const loadMoreData = vi.fn() + renderShowMoreButton({ loadMoreData }) + const button = screen.getByRole("button") + // React Aria Components Button uses onPress which listens to click events + fireEvent.click(button) + expect(loadMoreData).toHaveBeenCalledTimes(1) + }) + + it("renders down arrow icon when showLess is false", () => { + const loadMoreData = vi.fn() + renderShowMoreButton({ loadMoreData, showLess: false }) + const icon = screen.getByTestId("MaterialIcon") + expect(icon).toBeTruthy() + expect(icon.textContent).toBe("keyboard_arrow_down") + }) + + it("renders up arrow icon when showLess is true", () => { + const loadMoreData = vi.fn() + renderShowMoreButton({ loadMoreData, showLess: true }) + const icon = screen.getByTestId("MaterialIcon") + expect(icon).toBeTruthy() + expect(icon.textContent).toBe("keyboard_arrow_up") + }) + + it("applies custom variant prop", () => { + const loadMoreData = vi.fn() + renderShowMoreButton({ loadMoreData, variant: "Primary" }) + const button = screen.getByRole("button") + expect(button).toBeTruthy() + }) + + it("applies custom color prop", () => { + const loadMoreData = vi.fn() + renderShowMoreButton({ loadMoreData, color: "Inverted" }) + const button = screen.getByRole("button") + expect(button).toBeTruthy() + }) + + it("applies custom size prop", () => { + const loadMoreData = vi.fn() + renderShowMoreButton({ loadMoreData, size: "lg" }) + const button = screen.getByRole("button") + expect(button).toBeTruthy() + }) + + it("passes through additional props to Button", () => { + const loadMoreData = vi.fn() + renderShowMoreButton({ + loadMoreData, + "aria-label": "Custom label", + }) + const button = screen.getByRole("button") + expect(button.getAttribute("aria-label")).toBe("Custom label") + }) + }) +}) diff --git a/apps/scandic-web/components/TempDesignSystem/ShowMoreButton/index.tsx b/packages/design-system/lib/components/ShowMoreButton/ShowMoreButton.tsx similarity index 71% rename from apps/scandic-web/components/TempDesignSystem/ShowMoreButton/index.tsx rename to packages/design-system/lib/components/ShowMoreButton/ShowMoreButton.tsx index 80d0f86d8..264ad0426 100644 --- a/apps/scandic-web/components/TempDesignSystem/ShowMoreButton/index.tsx +++ b/packages/design-system/lib/components/ShowMoreButton/ShowMoreButton.tsx @@ -2,19 +2,12 @@ import { useIntl } from "react-intl" -import { Button } from "@scandic-hotels/design-system/Button" -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Button } from "../Button" +import { MaterialIcon } from "../Icons/MaterialIcon" -import type { ComponentProps } from "react" +import type { ShowMoreButtonProps } from "./types" -interface ShowMoreButtonProps extends ComponentProps { - showLess?: boolean - textShowMore?: string - textShowLess?: string - loadMoreData: () => void -} - -export default function ShowMoreButton({ +export function ShowMoreButton({ variant = "Text", color = "Primary", size = "md", diff --git a/packages/design-system/lib/components/ShowMoreButton/index.tsx b/packages/design-system/lib/components/ShowMoreButton/index.tsx new file mode 100644 index 000000000..e2e2d3bd9 --- /dev/null +++ b/packages/design-system/lib/components/ShowMoreButton/index.tsx @@ -0,0 +1,2 @@ +export { ShowMoreButton } from "./ShowMoreButton" +export { type ShowMoreButtonProps } from "./types" diff --git a/packages/design-system/lib/components/ShowMoreButton/types.ts b/packages/design-system/lib/components/ShowMoreButton/types.ts new file mode 100644 index 000000000..fdfb99457 --- /dev/null +++ b/packages/design-system/lib/components/ShowMoreButton/types.ts @@ -0,0 +1,11 @@ +import type { ButtonProps } from "../Button/types" + +export interface ShowMoreButtonProps extends Omit< + ButtonProps, + "children" | "onPress" +> { + showLess?: boolean + textShowMore?: string + textShowLess?: string + loadMoreData: () => void +} diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 42dd72587..2df56f6ca 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -167,6 +167,7 @@ "./Radio": "./lib/components/Radio/index.tsx", "./RegularRateCard": "./lib/components/RateCard/Regular/index.tsx", "./Select": "./lib/components/Select/index.tsx", + "./ShowMoreButton": "./lib/components/ShowMoreButton/index.tsx", "./SidePeek": "./lib/components/SidePeek/index.tsx", "./SidePeek/SidePeekProvider": "./lib/components/SidePeek/SidePeekContext/SidePeekProvider.tsx", "./SidePeekSelfControlled": "./lib/components/SidePeek/SelfControlled.tsx",