diff --git a/playbook/app/pb_kits/playbook/pb_timeline/_item.tsx b/playbook/app/pb_kits/playbook/pb_timeline/_item.tsx index 3d6ef9eafd..f5aa0d6d2b 100644 --- a/playbook/app/pb_kits/playbook/pb_timeline/_item.tsx +++ b/playbook/app/pb_kits/playbook/pb_timeline/_item.tsx @@ -1,12 +1,15 @@ import React from 'react' import classnames from 'classnames' - import { buildCss, buildHtmlProps } from '../utilities/props' -import { globalProps, GlobalProps } from "../utilities/globalProps"; +import { globalProps, GlobalProps } from "../utilities/globalProps" import DateStacked from '../pb_date_stacked/_date_stacked' import IconCircle from '../pb_icon_circle/_icon_circle' +import TimelineLabel from './subcomponents/Label' +import TimelineStep from './subcomponents/Step' +import TimelineDetail from './subcomponents/Detail' + type ItemProps = { className?: string, children?: React.ReactNode[] | React.ReactNode, @@ -17,6 +20,13 @@ type ItemProps = { lineStyle?: 'solid' | 'dotted', } & GlobalProps +function isElementOfType

( + element: React.ReactNode, + component: React.ComponentType

+): element is React.ReactElement

{ + return React.isValidElement

(element) && element.type === component +} + const TimelineItem = ({ className, children, @@ -31,31 +41,57 @@ const TimelineItem = ({ const htmlProps = buildHtmlProps(htmlOptions) + const childrenArray = React.Children.toArray(children) + + const labelChild = childrenArray.find( + (child): child is React.ReactElement => isElementOfType(child, TimelineLabel) + ) + + const stepChild = childrenArray.find( + (child): child is React.ReactElement => isElementOfType(child, TimelineStep) + ) + + const detailChild = childrenArray.find( + (child): child is React.ReactElement => isElementOfType(child, TimelineDetail) + ) + + const otherChildren = childrenArray.filter( + (child) => + !isElementOfType(child, TimelineLabel) && + !isElementOfType(child, TimelineStep) && + !isElementOfType(child, TimelineDetail) + ) + return ( -

-
- {date && - - } -
-
- -
-
-
- {children} -
+ {labelChild || ( +
+ {date && ( + + )} +
+ )} + {stepChild || ( +
+ +
+
+ )} + {detailChild || ( +
+ { otherChildren } +
+ )}
) } diff --git a/playbook/app/pb_kits/playbook/pb_timeline/_timeline.tsx b/playbook/app/pb_kits/playbook/pb_timeline/_timeline.tsx index e58d9f8acb..a0e3f53af6 100644 --- a/playbook/app/pb_kits/playbook/pb_timeline/_timeline.tsx +++ b/playbook/app/pb_kits/playbook/pb_timeline/_timeline.tsx @@ -5,6 +5,11 @@ import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from '../uti import { GlobalProps, globalProps } from '../utilities/globalProps' import TimelineItem from './_item' +import { + TimelineStep, + TimelineLabel, + TimelineDetail, +} from './subcomponents' type TimelineProps = { aria?: { [key: string]: string }, @@ -47,5 +52,8 @@ const Timeline = ({ } Timeline.Item = TimelineItem +Timeline.Step = TimelineStep +Timeline.Label = TimelineLabel +Timeline.Detail = TimelineDetail export default Timeline diff --git a/playbook/app/pb_kits/playbook/pb_timeline/detail.html.erb b/playbook/app/pb_kits/playbook/pb_timeline/detail.html.erb new file mode 100644 index 0000000000..919544cc91 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/detail.html.erb @@ -0,0 +1,3 @@ +<%= pb_content_tag do %> + <%= content.presence %> +<% end %> diff --git a/playbook/app/pb_kits/playbook/pb_timeline/detail.rb b/playbook/app/pb_kits/playbook/pb_timeline/detail.rb new file mode 100644 index 0000000000..810248dae4 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/detail.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Playbook + module PbTimeline + class Detail < Playbook::KitBase + def classname + generate_classname("pb_timeline_item_right_block") + end + end + end +end diff --git a/playbook/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_children.html.erb b/playbook/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_children.html.erb new file mode 100644 index 0000000000..1725989c17 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_children.html.erb @@ -0,0 +1,43 @@ +<%= pb_rails("timeline", props: {orientation: "horizontal", show_date: true}) do %> + <%= pb_rails("timeline/item", props: { line_style: "solid"}) do |item| %> + + <% item.label do %> + <%= pb_rails("timeline/label") do %> + <%= pb_rails("title", props: { text: "Any Kit Here", size: 2 }) %> + <% end %> + <% end %> + + <% item.step do %> + <%= pb_rails("timeline/step", props: { icon: 'check', icon_color: 'teal' }) %> + <% end %> + + <% item.detail do %> + <%= pb_rails("title_detail", props: { + title: "Jackson Heights", + detail: "37-27 74th Street" + }) %> + <% end %> + <% end %> + <%= pb_rails("timeline/item", props: { line_style: "dotted"}) do |item| %> + + <% item.step do %> + <%= pb_rails("timeline/step") do %> + <%= pb_rails("pill", props: { text: "Any Kit" , variant: "success" }) %> + <% end %> + <% end %> + + <% item.detail do %> + <%= pb_rails("title_detail", props: { + title: "Greenpoint", + detail: "81 Gate St Brooklyn" + }) %> + <% end %> + <% end %> + + <%= pb_rails("timeline/item", props: {icon: "map-marker-alt", icon_color: "purple", date: Date.today+1 }) do |item| %> + <%= pb_rails("title_detail", props: { + title: "Society Hill", + detail: "72 E St Astoria" + }) %> + <% end %> +<% end %> diff --git a/playbook/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_children.jsx b/playbook/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_children.jsx new file mode 100644 index 0000000000..c426fbc066 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_children.jsx @@ -0,0 +1,68 @@ +import React from 'react' + +import Timeline from '../_timeline' +import Title from '../../pb_title/_title' +import Pill from '../../pb_pill/_pill' + +import TitleDetail from '../../pb_title_detail/_title_detail' + +const TimelineWithChildren = (props) => ( +
+ + + + + </Timeline.Label> + <Timeline.Step icon="user" + iconColor="royal" + /> + <Timeline.Detail> + <TitleDetail detail="37-27 74th Street" + title="Jackson Heights" + {...props} + /> + </Timeline.Detail> + </Timeline.Item> + + <Timeline.Item lineStyle="dotted" + {...props} + > + <Timeline.Step> + <Pill text="Any Kit" + variant="success" + /> + </Timeline.Step> + <Timeline.Detail> + <TitleDetail detail="81 Gate St Brooklyn" + title="Greenpoint" + {...props} + /> + </Timeline.Detail> + </Timeline.Item> + + <Timeline.Item lineStyle="solid" + {...props} + > + <Timeline.Label date={new Date(new Date().setDate(new Date().getDate() + 1))} /> + <Timeline.Step icon="map-marker-alt" + iconColor="purple" + /> + <Timeline.Detail> + <TitleDetail detail="72 E St Astoria" + title="Society Hill" + {...props} + /> + </Timeline.Detail> + </Timeline.Item> + </Timeline> + </div> +) + +export default TimelineWithChildren diff --git a/playbook/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_children.md b/playbook/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_children.md new file mode 100644 index 0000000000..dc0264dd9e --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_children.md @@ -0,0 +1,2 @@ +Any kit can be used inside of our compound components of label, step, or detail. Expand the code snippet below to see how to use these children elements. + diff --git a/playbook/app/pb_kits/playbook/pb_timeline/docs/example.yml b/playbook/app/pb_kits/playbook/pb_timeline/docs/example.yml index 2cf2a99a73..1cd961b5d5 100644 --- a/playbook/app/pb_kits/playbook/pb_timeline/docs/example.yml +++ b/playbook/app/pb_kits/playbook/pb_timeline/docs/example.yml @@ -4,10 +4,11 @@ examples: - timeline_default: Default - timeline_vertical: Vertical - timeline_with_date: With Date + - timeline_with_children: With Children react: - timeline_default: Default - timeline_vertical: Vertical - timeline_with_date: With Date - + - timeline_with_children: With Children diff --git a/playbook/app/pb_kits/playbook/pb_timeline/docs/index.js b/playbook/app/pb_kits/playbook/pb_timeline/docs/index.js index 35398d22d6..8da0d1e1f0 100644 --- a/playbook/app/pb_kits/playbook/pb_timeline/docs/index.js +++ b/playbook/app/pb_kits/playbook/pb_timeline/docs/index.js @@ -1,3 +1,4 @@ export { default as TimelineDefault } from './_timeline_default.jsx' export { default as TimelineVertical } from './_timeline_vertical.jsx' export { default as TimelineWithDate } from './_timeline_with_date.jsx' +export { default as TimelineWithChildren } from './_timeline_with_children.jsx' diff --git a/playbook/app/pb_kits/playbook/pb_timeline/item.html.erb b/playbook/app/pb_kits/playbook/pb_timeline/item.html.erb index cb815cb6e2..8f7153b22b 100644 --- a/playbook/app/pb_kits/playbook/pb_timeline/item.html.erb +++ b/playbook/app/pb_kits/playbook/pb_timeline/item.html.erb @@ -1,25 +1,21 @@ <%= pb_content_tag do %> + <% if label %> + <%= label %> + <% else %> + <%= pb_rails("timeline/label", props: { date: date }) %> + <% end %> - <div class="pb_timeline_item_left_block"> - <% if object.date.present? %> - <%= pb_rails("date_stacked", props: { - date: object.date, - size: "sm", - align: "center" - }) %> - <% end %> - </div> - - <div class="pb_timeline_item_step"> - <%= pb_rails("icon_circle", props: { - icon: object.icon, - variant: object.icon_color, - size: "xs" - }) %> - <div class="pb_timeline_item_connector"></div> - </div> + <% if step %> + <%= step %> + <% else %> + <%= pb_rails("timeline/step", props: { icon: icon, icon_color: icon_color }) %> + <% end %> - <div class="pb_timeline_item_right_block"> - <%= content.presence %> - </div> + <% if detail%> + <%= detail%> + <% else %> + <%= pb_rails("timeline/detail") do %> + <%= content %> + <% end %> + <% end %> <% end %> diff --git a/playbook/app/pb_kits/playbook/pb_timeline/item.rb b/playbook/app/pb_kits/playbook/pb_timeline/item.rb index f5c5830777..9a954cc413 100644 --- a/playbook/app/pb_kits/playbook/pb_timeline/item.rb +++ b/playbook/app/pb_kits/playbook/pb_timeline/item.rb @@ -13,6 +13,10 @@ class Item < Playbook::KitBase values: %w[solid dotted], default: "solid" + renders_one :label + renders_one :step + renders_one :detail + def classname generate_classname("pb_timeline_item_kit", line_style) end diff --git a/playbook/app/pb_kits/playbook/pb_timeline/label.html.erb b/playbook/app/pb_kits/playbook/pb_timeline/label.html.erb new file mode 100644 index 0000000000..c7b4b3b18b --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/label.html.erb @@ -0,0 +1,12 @@ +<%= pb_content_tag do %> + <% if object.date.present? %> + <%= pb_rails("date_stacked", props: { + date: object.date, + size: "sm", + align: "center" + }) %> + <% else %> + <%= content.presence %> + <% end %> +<% end %> + diff --git a/playbook/app/pb_kits/playbook/pb_timeline/label.rb b/playbook/app/pb_kits/playbook/pb_timeline/label.rb new file mode 100644 index 0000000000..bfb74a469f --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/label.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Playbook + module PbTimeline + class Label < Playbook::KitBase + prop :date + + def classname + generate_classname("pb_timeline_item_left_block") + end + end + end +end diff --git a/playbook/app/pb_kits/playbook/pb_timeline/step.html.erb b/playbook/app/pb_kits/playbook/pb_timeline/step.html.erb new file mode 100644 index 0000000000..533e1cb383 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/step.html.erb @@ -0,0 +1,14 @@ +<%= pb_content_tag do %> + <% if object.icon.present? %> + <%= pb_rails("icon_circle", props: { + icon: object.icon, + variant: object.icon_color, + size: "xs" + }) %> + <% else %> + <%= content.presence %> + <% end %> + <div class="pb_timeline_item_connector"></div> +<% end %> + + diff --git a/playbook/app/pb_kits/playbook/pb_timeline/step.rb b/playbook/app/pb_kits/playbook/pb_timeline/step.rb new file mode 100644 index 0000000000..a80d28d51a --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/step.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Playbook + module PbTimeline + class Step < Playbook::KitBase + prop :icon, type: Playbook::Props::String + prop :icon_color, type: Playbook::Props::Enum, + values: %w[default royal blue purple teal red yellow green], + default: "default" + + def classname + generate_classname("pb_timeline_item_step") + end + end + end +end diff --git a/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/Detail.tsx b/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/Detail.tsx new file mode 100644 index 0000000000..0365376132 --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/Detail.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import classnames from 'classnames' +import { buildHtmlProps } from '../../utilities/props' +import { globalProps, GlobalProps } from "../../utilities/globalProps" + +type TimelineDetailProps = { + children?: React.ReactNode, + className?: string, + htmlOptions?: { [key: string]: any }, +} & GlobalProps + +const TimelineDetail: React.FC<TimelineDetailProps> = ({ + children, + className, + htmlOptions = {}, + ...props +}) => { + const htmlProps = buildHtmlProps(htmlOptions) + return ( + <div + {...htmlProps} + className={classnames('pb_timeline_item_right_block', globalProps(props), className)} + > + {children} + </div> + ) +} + +export default TimelineDetail diff --git a/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/Label.tsx b/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/Label.tsx new file mode 100644 index 0000000000..717e92aeaf --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/Label.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import classnames from 'classnames' +import { buildHtmlProps } from '../../utilities/props' +import { globalProps, GlobalProps } from "../../utilities/globalProps" +import DateStacked from '../../pb_date_stacked/_date_stacked' + +type TimelineLabelProps = { + date?: Date, + children?: React.ReactNode, + className?: string, + htmlOptions?: { [key: string]: any }, +} & GlobalProps + +const TimelineLabel: React.FC<TimelineLabelProps> = ({ + date, + children, + className, + htmlOptions = {}, + ...props +}) => { + const htmlProps = buildHtmlProps(htmlOptions) + return ( + <div + {...htmlProps} + className={classnames('pb_timeline_item_left_block', globalProps(props), className)} + > + {children} + {date && ( + <DateStacked align="center" + date={date} + size="sm" + /> + )} + </div> + ) +} + +export default TimelineLabel diff --git a/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/Step.tsx b/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/Step.tsx new file mode 100644 index 0000000000..648c1d9b0a --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/Step.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import classnames from 'classnames' +import { buildHtmlProps } from '../../utilities/props' +import { globalProps, GlobalProps } from "../../utilities/globalProps" +import IconCircle from '../../pb_icon_circle/_icon_circle' + +type TimelineStepProps = { + icon?: string, + iconColor?: 'default' | 'royal' | 'blue' | 'purple' | 'teal' | 'red' | 'yellow' | 'green', + children?: React.ReactNode, + className?: string, + htmlOptions?: { [key: string]: any }, +} & GlobalProps + +const TimelineStep: React.FC<TimelineStepProps> = ({ + icon = 'user', + iconColor = 'default', + children, + className, + htmlOptions = {}, + ...props +}) => { + const htmlProps = buildHtmlProps(htmlOptions) + return ( + <div + {...htmlProps} + className={classnames('pb_timeline_item_step', globalProps(props), className)} + > + {children ? ( + children + ) : ( + <IconCircle icon={icon} + size="xs" + variant={iconColor} + /> + )} + <div className="pb_timeline_item_connector" /> + </div> + ) +} + +export default TimelineStep diff --git a/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/index.tsx b/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/index.tsx new file mode 100644 index 0000000000..693e027a7d --- /dev/null +++ b/playbook/app/pb_kits/playbook/pb_timeline/subcomponents/index.tsx @@ -0,0 +1,3 @@ +export { default as TimelineLabel } from './Label'; +export { default as TimelineDetail } from './Detail'; +export { default as TimelineStep } from './Step'; diff --git a/playbook/app/pb_kits/playbook/pb_timeline/timeline.test.js b/playbook/app/pb_kits/playbook/pb_timeline/timeline.test.js index fa71db5bad..9468e2070d 100644 --- a/playbook/app/pb_kits/playbook/pb_timeline/timeline.test.js +++ b/playbook/app/pb_kits/playbook/pb_timeline/timeline.test.js @@ -2,6 +2,10 @@ import React from 'react' import { render, screen } from '../utilities/test-utils' import Timeline from './_timeline' +import TimelineItem from './_item' +import TimelineLabel from './subcomponents/Label' +import TimelineStep from './subcomponents/Step' +import TimelineDetail from './subcomponents/Detail' import TitleDetail from '../pb_title_detail/_title_detail' const testId = 'timeline' @@ -43,18 +47,91 @@ const TimelineDefault = (props) => ( </> ) +const TimelineWithChildren = (props) => ( + <> + <Timeline + className={className} + data={{ testid: testId }} + orientation="horizontal" + showDate + {...props} + > + <TimelineItem lineStyle="solid" + {...props} + > + <TimelineLabel date={new Date()} /> + <TimelineStep icon="user" + iconColor="royal" + /> + <TimelineDetail> + <TitleDetail + detail="37-27 74th Street" + title="Jackson Heights" + {...props} + /> + </TimelineDetail> + </TimelineItem> + + <TimelineItem lineStyle="dotted" + {...props} + > + <TimelineStep icon="check" + iconColor="teal" + /> + <TimelineDetail> + <TitleDetail + detail="81 Gate St Brooklyn" + title="Greenpoint" + {...props} + /> + </TimelineDetail> + </TimelineItem> + + <TimelineItem lineStyle="solid" + {...props} + > + <TimelineLabel + date={new Date(new Date().setDate(new Date().getDate() + 1))} + /> + <TimelineStep icon="map-marker-alt" + iconColor="purple" + /> + <TimelineDetail> + <TitleDetail + detail="72 E St Astoria" + title="Society Hill" + {...props} + /> + </TimelineDetail> + </TimelineItem> + </Timeline> + </> +) + test('should pass data prop', () => { render(<TimelineDefault />) const kit = screen.getByTestId(testId) expect(kit).toBeInTheDocument() }) +test('should pass data prop using children', () => { + render(<TimelineWithChildren />) + const kit = screen.getByTestId(testId) + expect(kit).toBeInTheDocument() +}) + test('should pass className prop', () => { render(<TimelineDefault />) const kit = screen.getByTestId(testId) expect(kit).toHaveClass(className) }) +test('should pass className prop with children', () => { + render(<TimelineWithChildren />) + const kit = screen.getByTestId(testId) + expect(kit).toHaveClass(className) +}) + test('should pass aria prop', () => { render(<TimelineDefault />) const kit = screen.getByTestId(testId) @@ -86,3 +163,10 @@ test('should pass showDate prop', () => { const kit = screen.getByTestId(testId) expect(kit).toHaveClass('pb_timeline_kit__horizontal__with_date') }) + +test('should pass showDate prop with Children', () => { + const props = { showDate: true } + render(<TimelineWithChildren {...props} />) + const kit = screen.getByTestId(testId) + expect(kit).toHaveClass('pb_timeline_kit__horizontal__with_date') +}) diff --git a/playbook/spec/pb_kits/playbook/kits/timeline_detail_spec.rb b/playbook/spec/pb_kits/playbook/kits/timeline_detail_spec.rb new file mode 100644 index 0000000000..fc77b24b5c --- /dev/null +++ b/playbook/spec/pb_kits/playbook/kits/timeline_detail_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "../../../../app/pb_kits/playbook/pb_timeline/detail" + +RSpec.describe Playbook::PbTimeline::Detail do + subject { Playbook::PbTimeline::Detail } + + describe "#classname" do + it "returns the correct class name" do + expect(subject.new({}).classname).to eq "pb_timeline_item_right_block" + end + end +end diff --git a/playbook/spec/pb_kits/playbook/kits/timeline_label_spec.rb b/playbook/spec/pb_kits/playbook/kits/timeline_label_spec.rb new file mode 100644 index 0000000000..555837360d --- /dev/null +++ b/playbook/spec/pb_kits/playbook/kits/timeline_label_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative "../../../../app/pb_kits/playbook/pb_timeline/label" + +RSpec.describe Playbook::PbTimeline::Label do + subject { Playbook::PbTimeline::Label } + + it { is_expected.to define_prop(:date) } + + describe "#classname" do + it "returns the correct class name" do + expect(subject.new.classname).to eq "pb_timeline_item_left_block" + end + end +end diff --git a/playbook/spec/pb_kits/playbook/kits/timeline_step_spec.rb b/playbook/spec/pb_kits/playbook/kits/timeline_step_spec.rb new file mode 100644 index 0000000000..ecbe365c72 --- /dev/null +++ b/playbook/spec/pb_kits/playbook/kits/timeline_step_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "../../../../app/pb_kits/playbook/pb_timeline/step" + +RSpec.describe Playbook::PbTimeline::Step do + subject { Playbook::PbTimeline::Step } + + it { is_expected.to define_prop(:icon) } + + it { + is_expected.to define_enum_prop(:icon_color) + .with_default("default") + .with_values("default", "royal", "blue", "purple", "teal", "red", "yellow", "green") + } + + describe "#classname" do + it "returns the correct class name" do + expect(subject.new.classname).to eq "pb_timeline_item_step" + end + end +end